State management with Riverpod
Learning objectives
- You know how to manage state using providers and consumers.
Riverpod
Riverpod is a state management library for Dart and Flutter. In the present versions of the material, we use Riverpod's Flutter library -- flutter_riverpod
-- version 2.4.0
. While starting to use Riverpod in local development requires adding it to the project dependencies outlined in pubspec.yaml
, we'll still continue working directly in the browser environment.
The following example outlines an application where state management has been implemented using Riverpod.
Providers and consumers
Riverpod relies on the notion of providers and consumers. In a nutshell, providers store state (i.e. data), allow changing the state, and provide means to listen (or watch) for the changes to the state. Consumers, on the other hand, listen for the changes in the state and update shown contents if needed.
Key concepts
There are three key concepts in Riverpod: (1) providers, (2) provider scope, and (3) consumer widgets. Providers contain state, provider scope is used to encapsulate the application to provide widgets access to providers, and consumer widgets allow accessing the providers and rebuild shown contents based on the changes.
The above example demonstrates the use of all of these. At the beginning of the application, we create an instance of a StateProvider that contains the number zero.
final rotationProvider = StateProvider<int>((ref) => 0);
We wrap the MaterialApp
class with ProviderScope that stores the state of the created providers.
void main() {
runApp(ProviderScope(
child: MaterialApp(
home: Scaffold(body: Center(child: RotatingButtonWidget())))));
}
Finally, to create a widget that can access the state, we inherit the ConsumerWidget and create a build
function that takes the BuildContext
and a WidgetRef as arguments. The WidgetRef
allows access to providers in the application.
class RotatingButtonWidget extends ConsumerWidget {
double _radians(degrees) {
return degrees * 0.01745;
}
Widget build(BuildContext context, WidgetRef ref) {
final int rotation = ref.watch(rotationProvider);
return Transform.rotate(
angle: _radians(rotation),
child: ElevatedButton(
onPressed: () => ref
.watch(rotationProvider.notifier)
.update((state) => state = (state + 45) % 360),
child: const Text('Rotate!'),
),
);
}
}
Accessing specific providers is done by calling the watch
method of WidgetRef
. The method takes a provider as an argument and listens for changes in the provider. Whenever a change occurs, the build
method is called and the widget is rebuilt.
If we wish to update the state of a widget, we watch the notifier
property of a provider. The notifier has a method update
that is given a function that describes how the state in the provider should change. In our example, the update
method is given a function that increases the value in the provider -- the state -- by 45 (modulo 360).
onPressed: () => ref
.watch(rotationProvider.notifier)
.update((state) => state = (state + 45) % 360),
This, when the button is pressed, the state changes and the widget is rebuilt with an updated rotation.
Multiple consumers
Having multiple consumer for the same provider is straightforward. In the following example, we have a provider keeping track of a balance, one widget for showing the balance, and three instances of a widget used to add to the balance.
When you try out the program, you notice that pressing any of the add button widgets changes the state of the application, which leads to an update in the balance widget.
When considering the application as a widget tree (omitting some of the widgets), the application looks as follows. Note that, when we click on a button, only BalanceWidget
(and its children) is updated.
Multiple providers
Having multiple providers is similarly straightforward. The following example extends the previous example by adding a new provider that keeps track of events. Whenever a button is pressed, in addition to adjusting the balance, the event count is incremented by one. The balance widget has also been adjusted to show the event count in addition to the balance.
More complex state
The previous examples have considered relatively simple providers that have used numbers as state. Let's next look into building an application with a bit more complex state.
The following example outlines an application that has a provider that holds a list of strings. There is a separate widget for adding a new string to the list, and a separate widget for listing the strings in the list as Text
widgets.
When we try out the application, it seems that pressing the button does not have an effect. A good first approach into resolving the issue is adding print statements to the application to meaningful locations. One such location is the event adding content to the list -- when adding content, we also print the contents of the list.
When we try out the application, we see that new items are added to the list and the list grows after each press of the button. This effectively means that the update
method works as expected. Why, then, are the contents in the list not shown to the user?
Consumers watch for changes in the state, i.e. the value stored in the provider.
The culprit here is that while the consumer watches for changes in the state, adding contents to a list does not actually change the state. That is, the reference to the list, which is the value stored in the provider, remains the same even if new data is added to the list.
Thus, to create a change, we need to change the state. This is done by returning a new value (i.e. a new list) from the function given to the update
method, as shown below.
The statement [...state, 'A new row']
shown above creates a new list, with the contents of the old list (in the variable state
) at the beginning and the string literal "A new row" at the end.