![logo](https://i0.wp.com/resocoder.com/wp-content/uploads/2020/11/logo.png?resize=80%2C80&ssl=1)
Riverpod is not only about providing objects around the app. It comes bundled and closely integrated with StateNotifier
which is a state management class. It resembles the default Flutter's ValueNotifier
or even Cubit
from the Bloc package without the underlying streams. This kind of immutable state management is great for keeping unpleasant surprises at bay.
Building an app
![002-weather-news](https://i0.wp.com/resocoder.com/wp-content/uploads/2020/12/002-weather-news.png?resize=77%2C77&ssl=1)
It has become a habit on Reso Coder to demonstrate different state management solutions by building a simple weather forecast app. Grab the starter project to follow along without writing any unnecessary code. Also, this tutorial assumes that you understand the basics of working with Riverpod.
The app displays a randomly generated temperature in the given city which allows us to demonstrate asynchronous fetching of data. We'll also show a loading indicator while awaiting a Future
and an error snackbar whenever an exception is thrown while fetching the forecast.
Let's now quickly take a look at the implemented classes from the starter project. If you haven't seen the simple weather forecast app in some of my previous tutorials, you should know that it's centered around a model class called Weather
.
weather.dart
class Weather {
final String cityName;
final double temperatureCelsius;
Weather({
@required this.cityName,
@required this.temperatureCelsius,
});
// == and hashCode overrides...
}
This weather will contain a randomly generated temperature gotten from a FakeWeatherRepository
. There's also a chance that a NetworkException
will be thrown instead.
weather_repository.dart
abstract class WeatherRepository {
Future<Weather> fetchWeather(String cityName);
}
class FakeWeatherRepository implements WeatherRepository {
double cachedTempCelsius;
@override
Future<Weather> fetchWeather(String cityName) {
// Simulate network delay
return Future.delayed(
Duration(seconds: 1),
() {
final random = Random();
// Simulate some network exception
if (random.nextBool()) {
throw NetworkException();
}
// Since we're inside a fake repository, we need to cache the temperature
// in order to have the same one returned in for the detailed weather
cachedTempCelsius = 20 + random.nextInt(15) + random.nextDouble();
// Return "fetched" weather
return Weather(
cityName: cityName,
// Temperature between 20 and 35.99
temperatureCelsius: cachedTempCelsius,
);
},
);
}
}
class NetworkException implements Exception {}
Adding dependencies
Although state_notifier exists as a package on its own, riverpod comes bundled with it, so we need to have only a single dependency.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^0.12.1
Introduction to StateNotifier
![001-views](https://i0.wp.com/resocoder.com/wp-content/uploads/2020/12/001-views.png?resize=77%2C77&ssl=1)
The shortest explanation of a StateNotifier
is that it's a Flutter-independent ValueNotifier
- a sibling of the mutable ChangeNotifier
. If you have any experience with immutable state, you're going to feel right at home. If not, please, check out a separate tutorial about its core principles.
If you're familiar with the default Flutter ValueNotifier
, you can basically skip this section. The whole StateNotifier
revolves around one property called state
. This property can be listened to from the widgets using Riverpod Provider
classes. Whenever the state
property is set to a new value, all of its listeners are notified. In Flutter, this means that widgets are rebuilt. Neat!
A very simple StateNotifier
operating with a single int
as its state would look like this:
counter_notifier.dart
class CounterNotifier extends StateNotifier<int> {
// The value passed into the super constructor is the initial state, in this case, zero.
CounterNotifier() : super(0);
void increment() {
// Reassigning state
// Could also be written as `state = state + 1;`.
// Notifies all listeners about the state change.
state++;
}
}
A more complex state
In the weather forecast app we're building, having a single int
for a state just doesn't cut it. Whenever a state is complex, for example, consisting of multiple subclasses or a freezed union, you should write code for it first and only then worry about implementing the actual StateNotifier
.
We're going to be asynchronously loading a single resource - the Weather
model. In such occasions, it's best to represent the state as multiple subclasses of a WeatherState
abstract class.
In order to keep the code that relates to the StateNotifier
concise, let's put all of the states into application/weather_notifier.dart. All of the classes override their equality and hash code.
weather_notifier.dart
abstract class WeatherState {
const WeatherState();
}
class WeatherInitial extends WeatherState {
const WeatherInitial();
}
class WeatherLoading extends WeatherState {
const WeatherLoading();
}
class WeatherLoaded extends 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 extends WeatherState {
final String message;
const WeatherError(this.message);
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is WeatherError && o.message == message;
}
@override
int get hashCode => message.hashCode;
}
With the state classes written, we can now move to implementing the WeatherNotifier
which will be responsible for what I call the application logic - getting data from the already implemented WeatherRepository
and then updating the state
field, so that the UI can reflect the latest data.
weather_notifier.dart
class WeatherNotifier extends StateNotifier<WeatherState> {
final WeatherRepository _weatherRepository;
WeatherNotifier(this._weatherRepository) : super(WeatherInitial());
Future<void> getWeather(String cityName) async {
try {
state = WeatherLoading();
final weather = await _weatherRepository.fetchWeather(cityName);
state = WeatherLoaded(weather);
} on NetworkException {
state = WeatherError("Couldn't fetch weather. Is the device online?");
}
}
}
The initial state that is passed to the super constructor is unsurprisingly WeatherInitial
. The logic inside of the getWeather
method is quite self-explanatory.
Providers
![001-provider](https://i0.wp.com/resocoder.com/wp-content/uploads/2020/11/001-provider.png?resize=77%2C77&ssl=1)
Up to this point, we've been working only with the StateNotifier
class. Let's now utilize its close integration with a special Riverpod's StateNotifierProvider
in order to nicely rebuild the widgets whenever a new state is set.
providers.dart
final weatherRepositoryProvider = Provider<WeatherRepository>(
(ref) => FakeWeatherRepository(),
);
final weatherNotifierProvider = StateNotifierProvider(
(ref) => WeatherNotifier(ref.watch(weatherRepositoryProvider)),
);
User Interface
As with any project using Riverpod, we first need to wrap the whole app in a ProviderScope
.
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
title: 'Weather Search',
home: WeatherSearchPage(),
),
);
}
}
As you could have seen in the video at the beginning of this article, the app will either show only a city search bar for initial and error states, a progress indicator for the loading state, and lastly, the temperature and city name for the loaded state. Additionally, we want to show a SnackBar
if an error occurs.
The widgets representing each state are already prepared in the starter project. We just need to connect them up using a Consumer
.
watch
ing the weatherNotifierProvider
but instead a weatherNotifierProvider.state
. This is a special provider bundled inside of the StateNotifierProvider
that allows us to easily rebuild the widget tree when a new state is set.weather_search_page.dart
class WeatherSearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Weather Search"),
),
body: Container(
padding: EdgeInsets.symmetric(vertical: 16),
alignment: Alignment.center,
child: Consumer(
builder: (context, watch, child) {
final state = watch(weatherNotifierProvider.state);
if (state is WeatherInitial) {
return buildInitialInput();
} else if (state is WeatherLoading) {
return buildLoading();
} else if (state is WeatherLoaded) {
return buildColumnWithData(state.weather);
} else {
// (state is WeatherError)
return buildInitialInput();
}
},
),
),
);
}
// The build* methods are here...
}
We're still not showing the SnackBar
anywhere. But where should we put that code?
Anything that should be run only once when the state is updated has no place inside of the build
method directly as that can run many times over. This applies to navigating, showing SnackBars
and performing any side effects.
As if that wasn't enough, showing a SnackBar
directly from a build
method will result in the setState() or markNeedsBuild() called during build error message.
That's why there's a widget that allows you to listen to a provider outside of the build
method. Let's wrap the Consumer
with it.
weather_search_page.dart
ProviderListener<WeatherState>(
provider: weatherNotifierProvider.state,
onChange: (context, state) {
if (state is WeatherError) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
),
);
}
},
child: Consumer(
builder: (context, watch, child) {
// Previously written code here...
},
),
),
That's not all though! We're still not calling the getWeather
method from anywhere, so let's do so vrom the very bottom of the weather_search_page.dart file in the CityInputField
.
weather_search_page.dart
class CityInputField extends StatelessWidget {
// Code here...
void submitCityName(BuildContext context, String cityName) {
context.read(weatherNotifierProvider).getWeather(cityName);
}
}
Whereas we specify weatherNotifierProvider.state
for listening to state changes, when we want to call methods on our notifier, we read only the weatherNotifierProvider
itself.
And there you go! You've just finished a real-worldish app using the Riverpod + StateNotifier combo for state management. It's effective, immutable and clean. What more can you wish for?
Hey, the finished project is not up on github
My bad, it’s up there now.
Can you check in your code so we can see the final product?
Done!
Excellent tutorial implementing the Riverpod State Notifier package to the weather app.
Hello, Thank you for the excellent, state-of-the-art tutorials.
I have a question that perhaps is aimed at state management with StateNotifier:
How do you handle editable states? For example, what if the weather, after being loaded, was user editable? How would you set this up in your State and Notifier classes?
Eg: Let’s say you had an Edit Weather button that calls an editWeather(double newWeather) method. Where would the implementation of this method happen? How can you edit the Weather property of the state, if the state is of type WeatherState which may or may not have a Weather object depending on the current State?
I have posted this question now on StackOverflow as a general question as well (no answers so far) https://stackoverflow.com/questions/65282686/how-can-you-edit-update-part-of-state-using-bloc-pattern-with-statenotifier
Nice tutorial. I think cubit is still a cleaner approach compared to RiverPod + Statenotifier. What do you think?
Great tutorial. But I’m a bit confused on where you should initialize data of the widget (future, like getting current UID or fetching some data from firestore). I am doing it wrong in the buildInitial() since it gives me a “dirty state (parent/child)”.
Where should one fetch initial data of a widget?
Hi. I struggled with this because riverpod has a breaking change in 0.14.0, see https://riverpod.dev/docs/migration/0.13.0_to_0.14.0/. It would be great if you could point to this (or incorporate it). Thanks
weatherNotifierProvider.state when I try to put that .state ahead of my StateNotifierprovider. It does shows up.
Also, when I just put weatherNotifierProvider I get following error.
The argument type ‘StateNotifierProvider’ can’t be assigned to the parameter type ‘ProviderBase?’.
can you update this written docs to the new version of riverpod, please.
I have read some excellent stuff here. Definitely value bookmarking for revisiting. I wonder how much effort you put to make the sort of excellent informative website.
hello!,I really like your writing so a lot! share we keep up a correspondence extra approximately your post on AOL? I need an expert in this house to unravel my problem. May be that is you! Taking a look ahead to see you.
Simply wish to say your article is as amazing. The clearness in your post is just nice and i could assume you’re an expert on this subject. Well with your permission let me to grab your feed to keep updated with forthcoming post. Thanks a million and please carry on the gratifying work.
I loved as much as you will receive carried out right here. The sketch is attractive, your authored material stylish. nonetheless, you command get got an impatience over that you wish be delivering the following. unwell unquestionably come more formerly again since exactly the same nearly a lot often inside case you shield this hike.
Just wish to say your article is as surprising. The clearness in your post is just cool and i could assume you’re an expert on this subject. Fine with your permission allow me to grab your RSS feed to keep updated with forthcoming post. Thanks a million and please keep up the enjoyable work.
Magnificent beat ! I would like to apprentice while you amend your site, how can i subscribe for a blog web site? The account helped me a acceptable deal. I had been a little bit acquainted of this your broadcast offered bright clear idea
Excellent blog here! Also your website loads up very fast! What web host are you using? Can I get your affiliate link to your host? I wish my web site loaded up as quickly as yours lol
hi!,I like your writing so much! share we be in contact more approximately your article on AOL? I need a specialist in this area to resolve my problem. Maybe that is you! Looking ahead to see you.
Usually I do not read article on blogs, however I would like to say that this write-up very compelled me to take a look at and do it! Your writing style has been amazed me. Thank you, very nice article.
Hi, i think that i saw you visited my web site thus i came to ?eturn the favor텶’m attempting to find things to enhance my site!I suppose its ok to use a few of your ideas!!
I loved as much as you will receive carried out right here. The sketch is attractive, your authored material stylish. nonetheless, you command get got an impatience over that you wish be delivering the following. unwell unquestionably come more formerly again since exactly the same nearly a lot often inside case you shield this hike.
Fantastic site. Lots of helpful information here. I am sending it to some friends ans additionally sharing in delicious. And of course, thanks for your effort!
I do believe all the ideas you’ve presented for your post. They are really convincing and will certainly work. Nonetheless, the posts are too short for novices. May just you please lengthen them a little from subsequent time? Thanks for the post.
Experience Excellence with Bwer Pipes: Elevate your farming operations with Bwer Pipes’ industry-leading irrigation solutions. Our cutting-edge sprinkler technology and durable pipes are engineered for superior performance, enabling Iraqi farmers to achieve optimal water management, crop health, and productivity. Explore Bwer Pipes
Thanks for sharing. I read many of your blog posts, cool, your blog is very good.