— Flutter, State Management, Flutter Tips, Riverpod — 3 min read
I've kind of been on a tangent lately writing a few different posts about the state management package Riverpod(for Flutter/Dart). Today I'm writing yet another post where I'm going to breakdown the main different kinds of Providers that Riverpod has and how they can be used.
Riverpod is a pretty large library that has a reasonably steep learning curve. Probably even more so than the original Provider package. I think a lot of the difficulty mostly lies with all the different types of Providers, and not knowing what each one is useful for. So let's get into it.
This is pretty much the most basic kind of Provider. It can hold any type of data but it isn't very complex. Here's a quick example:
1final numberProvider = Provider((ref) => 1);
When I say it's not very complex, I more mean that it isn't very flexible. You can't change it from the outside, like somewhere in your Widget's build
method for example(you can use ref.watch
to monitor other Providers and change its value from inside it though).
This is a step up from a regular Provider
. It's not much more complex, but you can actually change its value from the outside. Here's an example:
1final numberProvider = StateProvider((ref) => 1);
This looks pretty much the same as what I wrote above but the difference is that when you use useProvider
in your HookWidget
you'll notice that it has a getter/setter called state
that can be used to read or manipulate its value:
1final int number = useProvider(numberProvider).state;
StateNotifierProvider is probably as complex as things can get. I wrote another tutorial that showed how to create a counter with Riverpod and StateNotifierProvider. You can read it here.
The general idea is that you first create a class that holds some kind of state:
1// To create a StateNotifier you first need to create class/model to hold the state2class CounterState {3
4 CounterState({this.value = 0});5 final int value;6
7 // This is just a simple utility method, you might want to try out freezed8 // for more complex implementations9 CounterState copyWith({int count}) {10 return CounterState(11 value: count ?? this.value12 );13 }14
15}
Then you create a StateNotifier that holds that state and makes updates/manipulations(update notifications are sent when you manipulate state
):
1class CounterNotifier extends StateNotifier<CounterState> {2 CounterNotifier() : super(CounterState());3
4 increase() => state = state.copyWith(count: state.value + 1);5 decrease() => state = state.copyWith(count: state.value - 1);6}
And then finally you call useProvider to monitor or manipulate the state:
1@override2 Widget build(BuildContext context) {3 final CounterState counterState = useProvider(counterProvider.state);4 final CounterNotifier counterNotifier = useProvider(counterProvider);5 ...6 }
The difference between StateNotifierProvider and Provider/StateProvider is that it can be used for more complex data. If you're just using a primitive like String
or int
you can probably get by with Provider or StateProvider but if you have a complicated Object like, for example, a class called User that has lots of different complex values inside it you'll need something more robust(unless you want code that is overly complicated and hard to test). That's where StateNotifierProvider comes in.
ChangeNotifierProvider was something that was used pretty widely in the original Provider package. In Riverpod it is kind of overshadowed by StateNotifierProvider, and probably for good reason because ChangeNotifier and ChangeNotifierProvider are locked to Flutter and can't be used in plain Dart apps. Even so, I still like ChangeNotifierProvider and I use it a lot to create View Models. The best way to describe it is probably 'a more basic StateNotifierProvider'.
First you make a model class that extends ChangeNotifier
:
1class CounterModel extends ChangeNotifier {2
3 int value = 0;4
5 void increase() {6 value = value + 1;7 notifyListeners();8 }9
10 void decrease() {11 value = value - 1;12 notifyListeners();13 }14
15}
Then you create the ChangeNotifierProvider:
1final counterModelProvider = ChangeNotifierProvider((ref) => CounterModel());
And use it inside your build method:
1final counterModel = useProvider(counterModelProvider);
The big difference here is that you need to manually notify for updates using the notifyListeners
method.
FutureProvider is used to calculate/generate asynchronous values, like if you were getting data from a web API or a local database - something that isn't synchronous or instantaneous:
1final userDataRepositoryProvider = Provider((ref) => UserDataRepository());2
3final usernameProvider = FutureProvider<String>((ref) async {4 final repository = ref.read(userDataRepositoryProvider);5 return await repository.getUsername();6});
One major drawback is you can't specific when FutureProvider will execute...It's almost automatic in a sense. You can get it to fire again with context.refresh
(an extension method) though.
StreamProvider is used to pick up updates from a Stream
, it's kind of like a FutureProvider
but it doesn't execute once and end - it is continuous. There's a lot of good use cases for this, an example might be a chat app where a client app is constantly receiving or polling for new messages. The official documentation for Riverpod actually has a really good WebSocket example for this, so I'll paste it here:
1final messageProvider = StreamProvider.autoDispose<String>((ref) async* {2 // Open the connection3 final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');4
5 // Close the connection when the stream is destroyed6 ref.onDispose(() => channel.sink.close());7
8 // Parse the value received and emit a Message instance9 await for (final value in channel.stream) {10 yield value.toString();11 }12});
And that about sums it up. There might be a couple of different Provider types I haven't mentioned here but I think I've covered the major ones and use cases. If you want to read more about Riverpod, the official documentation is pretty good too - you can check it out here.