From SetState to Riverpod: Flutter state management
State management is one of the most important topic while starting to build apps with flutter. You’ll always create production apps with interaction from users or any external sources and thus the state is going to change a lot of times.
There are many state management techniques having their own advantages and use cases. You can check this link to know about the different state management techniques available.
Meanwhile, if you don’t have any idea about state in flutter, I suggest you to check this flutter docs before continuing with this article.
First, let’s see what we would be building at the end of this article. It’s a simple app in which a container changes its color and shape by clicking a button.
You might think this can be easily achieved in flutter by using setState, then why use state management techniques like Riverpod?.
Well, there is no issue in using setState with simple apps like this, simple in the sense that currently, this app contains only a single widget which needs to be rebuilt by setState, performance issues might start when there are multiple widgets say in a ListView or Column which needs to be rebuilt.
For Example…
int _value = 10;@override
Widget build(BuildContext context) {
return Column(
children: [
const Widget1("$_value"),
const Widget2(),
const Widget3(),
ElevatedButton(
child: const Text("Update"),
onPressed: () => setState(() {
_value += 10;
}),
),
],
);
}
In this code sample, although only Widget1 needs to be rebuilt but setState will rebuild the entire Column Widget i.e. even Widget2 and Widget3 will be rebuilt, costing you in terms of performance.
You can use different methods to solve this issue like InheritedWidget
or Provider
package, but we will be using Riverpod
to manage state in our app.
Advantages of using Riverpod
Enough of theory 😰, let’s start building our app 😄 🚀.
Add the following dependency into your pubspec.yaml
dependencies: flutter_riverpod: <latest_version>
Import the package into your main.dart
file.
import 'package:flutter_riverpod/flutter_riverpod.dart';
Now to make riverpod work with flutter, because it is independent of flutter and can also run with dart files, we need to wrap our top-level widget MyApp()
with another widget i.e. ProviderScope.
void main() {
runApp(
const ProviderScope(
child: MyApp(),
)
);
}
Now create a basic MyApp and HomeWidget widget that contains a button to navigate to the next page. This is how your main.dart
should look like.
Now let’s create a file in lib
folder as providers.dart
which will contain all our changeable variables like height, width of the container etc. (All values which will change)
final heightProvider = StateProvider((ref) => 100.0);
This is how we create a provider and give it an initial default value of 100.0. One thing which you should note is that these providers do not require to be enclosed in a class or a method and can be directly accessed throughout the app.
Create remaining providers for other variables. This is how your providers.dart
should look like.
We need the colorProvider
to change the color of the container and the radiusProvider
to change the radius of the container.
Next step is to create cont.dart
file and a AnimatedBox
widget inside it.
import 'package:flutter/material.dart';class AnimatedBox extends StatelessWidget {
const AnimatedBox({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
color: Colors.red,
height: 100,
width: 100,
),
),
),
);
}
}
This is how your app should look till now.
Now before we can start accessing and modifying our providers, we need to change the type of AnimatedBox
widget from StatelessWidget
to ConsumerWidget
ConsumerWidget
is an abstract class which extends ConsumerStatefulWidget
which further extends a StatefulWidget
. When you change the widget into a ConsumerWidget
, you need to give one more parameter to build
method which is of type WidgetRef
to access providers using the ref
.
Now you need to access the providers present in provider.dart
file.
final double height = ref.watch(heightProvider);
This is how you can access all the providers using the ref
method which returns the value exposed by a provider and rebuild the widget when that value changes.
Similarly, access all the providers in your build method.
@overrideWidget build(BuildContext context, WidgetRef ref) { final double height = ref.watch(heightProvider);
final double width = ref.watch(widthProvider); final int red = ref.watch(redProvider);
final int blue = ref.watch(blueProvider);
final int green = ref.watch(greenProvider); final double radius = ref.watch(radiusProvider); final maxHeight = MediaQuery.of(context).size.height; // get the upper limit of height
final maxWidth = MediaQuery.of(context).size.width; // get the upper limit of width.....
Now you can simply access the value of providers using the respective variables created. Let’s show the current value of height and width in the AppBar
.
appBar: AppBar(
title: Column(
children: [
Text(
'Height: $height',
),
Text(
'Width: $width',
),
],
),
),
Assign the properties of container such as height, width, color.
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: height,
width: width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
color: Color.fromARGB(255, red, green, blue),
),
),
),
),
And create a FloatingActionButton
to trigger changes in the container.
floatingActionButton: FloatingActionButton(
child: const Icon (Icons.refresh),
onPressed: () {}
),
Now the most interesting part, we need to modify the value of providers so that the shape and size of the container changes. We can generate random numbers to update the properties but how do we update the values?
For updating the provider's value, we can use ref.read().
It reads a provider without listening to it.
onPressed: () {
ref.read(heightProvider.notifier).state = Random().nextInt(maxHeight.toInt()).toDouble();
}
You might have observed that we are using the .notifier
on the provider to change the value, this is because .read
only returns the value of the provider locally, but we need to update so that it is reflected in every part of the app where the provider is used.
.notifier
obtains the [StateController] associated with this provider, but without listening to it.
.state
is the current “state” of this [StateNotifier]. Updating this variable will synchronously call all the listeners. Notifying the listeners is O(N) with N the number of listeners.
Now let’s update all the providers one by one.
onPressed: () {
ref.read(heightProvider.notifier).state = Random().nextInt(maxHeight.toInt()).toDouble();ref.read(widthProvider.notifier).state = Random().nextInt(maxWidth.toInt()).toDouble();ref.read(redProvider.notifier).state = Random().nextInt(255);ref.read(greenProvider.notifier).state = Random().nextInt(255);ref.read(blueProvider.notifier).state = Random().nextInt(255);ref.read(radiusProvider.notifier).state = Random().nextInt(360).toDouble();},
Your co.dart
should look something like this.
Moment of truth, Let’s run our app to see if it works as expected.
It’s working!! 🎉, you might have noticed that the state is being preserved even if you navigate back to the main screen. There are two methods to reset the value of providers to their default value...
- Call
autodispose
to automatically reset values after navigating back. - Explicitly reset the values.
To call autoDispose
automatically, go to the declaration of provider and replace StateProvider
with StateProvider.autoDispose
.
final heightProvider = StateProvider.autoDispose((ref) => 100.0);// update remaining providers
This results in….
Now to explicitly reset the values we can use ref.refresh
which forces a provider to re-evaluate its state immediately and return the created value.
Add the action
parameter in the AppBar
actions: [
IconButton(
onPressed: () {
ref.refresh(heightProvider);
ref.refresh(widthProvider);
ref.refresh(blueProvider);
ref.refresh(greenProvider);
ref.refresh(redProvider);
},
icon: const Icon(Icons.restore))
],
This is the final result…
Well, that’s it for this article. This may seem over-engineered for a simple app like this but the concepts of riverpod will be useful while building and designing production-level apps.
Here’s the GitHub link for the above app.