From SetState to Riverpod: Flutter state management

Rohit Ranjan
GDSC GHRCE
Published in
7 min readJun 23, 2022

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.

An app in which a container changes it color and shape

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 Riverpodto manage state in our app.

Advantages of using Riverpod

Images from the official Riverpod website

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.dartfile.

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 radiusProviderto 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 .

Change StatlessWidget to ConsumerWidget.

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 refmethod 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.

Updating values of providers

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….

Using AutoDispose

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 actionparameter 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…

Explicitly resetting the values of providers

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.

--

--