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
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
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
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.
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
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.");
}
}
}
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>
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
final weatherNotifierProvider = StateNotifierProvider(
(ref) => WeatherNotifier(ref.watch(weatherRepositoryProvider)),
);
Implementing the flutter UI
Wrap the Material app in ProviderScope
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
// 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!');
},
),
],
),
),
);
}
}
Leave a Reply