Flutter_riverpod package

I am writing this post, because I belive it is important to have some solid tutorial that explains something little simpler than the rest.

I learned the things that i know from reso coder and other youtube tutorials

What is Riverpod?

Riverpod is a state management solution for flutter/dart.

It is scalable and if you follow some design principles, it is robust and organised.

In this guide, we will be using flutter_riverpod package, but there are also riverpod and hooks_riverpod, but we wont be covering those here

Implementing it in project

Import dependency of flutter_riverpod

flutter pub add flutter_riverpod

Create the model of the data stored

in our example we would make Weather App, so it is weather in some location

Dart
class Weather {
  final String cityName;
  final double temperature;
  Weather({
    required this.cityName,
    required this.temperature,
  });
}

Create WeatherState

we will make an absctract class, that could not be instanciated, but can be implemented by others, it is used here, because we can also say that our state will be WeatherState() and it could be all of the others, that implement it

Dart
abstract class WeatherState {
  const WeatherState();
}

and under it, create all the possible states, with the WeatherLoaded actualy carrying Weather as a Parameter, and WeatherError carrying message

Dart
class WeatherInitial implements WeatherState {
  const WeatherInitial();
}

class WeatherLoading implements WeatherState {
  const WeatherLoading();
}

class WeatherLoaded implements WeatherState {
  final Weather weather;
  const WeatherLoaded(this.weather);
  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;
    return o is WeatherLoaded && o.weather == weather;
  }

  @override
  int get hashCode => weather.hashCode;
}

class WeatherError implements WeatherState {
  final String message;
  const WeatherError(this.message);
}

we should also @override equals on two so that we could differenciate changes by actual value, and not by its reference
also we @override the hashcode getter, so that we would not bump in any errors that could occur in some rare cases

Create WeatherRepository

we need to get the data, from the Some backend API, or from fake one.
we make the WeatherRepository abstract, so that we know what methods we need to implement, but in real app scenario, we would not make it abstract, as it would probably be implementing the fetching from real backend, and Fake repository could be exactly the same.

Dart
abstract class WeatherRepository {
  Future<Weather> fetchWeather(String city);
}

class FakeWeatherRepository implements WeatherRepository {
  @override
  Future<Weather> fetchWeather(String city) {
    //job of getting the data from the backend
    //delay one second to simulate the network delay
    return Future.delayed(
      const Duration(seconds: 1),
      () {
        Random random = Random();
        if (random.nextBool()) {
          throw Exception();
        }
        //throw Exception();
        int randomTemperature = random.nextInt(40);
        return Weather(
            cityName: city, temperature: randomTemperature.toDouble());
      },
    );
  }
}

Create WeatherNotifier

class that extends StateNotifier<WeatherState> that says to the Flutter widgets what actions we can do and holds the Single state

Dart
class WeatherNotifier extends StateNotifier<WeatherState> {
  final WeatherRepository _weatherRepository;
  WeatherNotifier(this._weatherRepository) : super(const WeatherInitial());
  Future<void> fetchWeather(String cityName) async {
    state = const WeatherLoading();
    try {
      final weather = await _weatherRepository.fetchWeather(cityName);
      state = WeatherLoaded(weather);
    } on Exception catch (_) {
      state = const WeatherError("Couldn't fetch weather.");
    }
  }
}
Dart
 WeatherNotifier(this._weatherRepository) : super(const WeatherInitial());

WeatherNotifier(this._weatherRepository)
is the inicializer part, that when you instanciate it later in a provider, you can say, that it uses fake or real weather repository.

we use super(const WeatherInitial()) to call the constructor of the parent class to initialize the properties from the parent class- Shortly, initial state

Add providers

weather repository provider for providing us with the data fetching
it uses Provider<repository>

Dart
final weatherRepositoryProvider = Provider<WeatherRepository>(
  (ref) => FakeWeatherRepository(),
);

weatherNotifierProvider which uses StateNotifier Provider with a ref.watch on the state of the weatherRepositoryProvider that we inicialized in the step before

Dart
final weatherNotifierProvider = StateNotifierProvider(
  (ref) => WeatherNotifier(ref.watch(weatherRepositoryProvider)),
);

Implementing the flutter UI

Wrap the Material app in ProviderScope

Dart
ProviderScope(
      child: MaterialApp(
        title: 'flutter_riverpod_example',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        home: const ExampleHomePage(),
      ),
    );
  }

Create ExampleHomePage

because we are using riverpod, we can now in most cases just use StatelessWidgets
use StatefullWidgets only for the components with some input or functionality that is not implemented using riverpod

Dart
// stateless widget, because there is only one TextField, and it can use onSubmitted instead of having custom TextEditingController
class ExampleHomePage extends StatelessWidget {
  const ExampleHomePage({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
  // we use scaffold to make a whole new page
    return Scaffold(
    // we do not need appbar, so we go straignt into implementation of Body
    // Center all child widgets
      body: Center(
      // child should be column, and the height should be minimum, based on         content
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // consumer, so we would get the ref and get the WeatherNotifier
           
            Consumer(builder: (context, ref, child) {
             // location input
              return TextField(
                decoration: const InputDecoration(
                  hintText: 'Enter a city',
                  border: OutlineInputBorder(),
                ),
                onSubmitted: (value) {
                // we get the method for handling the fetching and setting of states from weatherNotifierProvider
                // we want to only read the notifier because watch could trigger rebuilds that are not needed
                  ref
                      .read(weatherNotifierProvider.notifier)
                      .fetchWeather(value);
                },
              );
            }),
            // we use consumer for the weather State
            // it is important to use watch so it rerenders on change
            // for type safe code, we have to ask, if the WeatherState is initial, loading, error, or loaded and handle the actions accordingly, if we would not check if it is the state type, we would get error, that the state.weather is undefined.
            Consumer(
              builder: (context, ref, child) {
                final state = ref.watch(weatherNotifierProvider);
                if (state is WeatherInitial) {
                  return const Text('Please Select a Location');
                }
                if (state is WeatherLoading) {
                  return const CircularProgressIndicator();
                }
                if (state is WeatherLoaded) {
                  final weather = state.weather;
                  return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        weather.cityName,
                        style: Theme.of(context).textTheme.headlineMedium,
                      ),
                      Text(
                        '${weather.temperature.toStringAsFixed(1)} °C',
                        style: Theme.of(context).textTheme.headlineLarge,
                      ),
                      const SizedBox(height: 20),
                    ],
                  );
                }
                if (state is WeatherError) {
                  return Text(
                    state.message,
                    style: const TextStyle(color: Colors.red),
                  );
                }
                return const Text('Something went wrong!');
              },
            ),
          ],
        ),
      ),
    );
  }
}

If something is not clear, or outdated?
Comment it, so that we could all get better!


Posted

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Search