Flutter - Introduction to State Management Using Riverpod
Last Updated :
07 Sep, 2021
Riverpod is a Reactive State-Management and Dependency Injection framework, it uses different providers to let us access and listen to state changes across our app, it is built by Remi Rousselet. If you don't know what is state then I'll recommend you to read this article first as it will be a little hard for you to understand Riverpod without understanding the "state" itself.
What does Riverpod actually do?
Riverpod is a state management helper. It basically makes our state (in clear words, our variables' values) accessible in all parts of the app, it puts our state at the top of the widget tree and lets us listen to those state changes and update our User Interface accordingly.
There are many types of providers which Riverpod offers, We’ll go over them one by one.
What are Providers?
Providers are the most important part of a Riverpod application, “A provider is an object that encapsulates a piece of state and allows listening to that state.”
Types of Providers:
- StateProvider
- FutureProvider
- StreamProvider
- Provider
Let's start building!
1. Add Riverpod to our app:
Dart
dependencies:
flutter_riverpod: ^0.14.0+3
2. Wrapping our App:
For providers to work, we must add ProviderScope at the root of our Flutter applications:
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(ProviderScope(child: MyApp()));
}
3. Create the providers.dart file:
We'll define all our global providers in this file so that it is easier to maintain the project. Let's define a StateProvider.
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Instead of String you can use other data types as well,
// we can also use custom data types.
final userNameProvider=StateProvider<String>((ref) {
// we can also return an empty String here, for the sake of simplicity,
// let's return a sample name
return "SomeName";
});
4. Reading Providers:
Now that we’ve declared a StateProvider, Let’s learn how can we read providers. To read any provider, we have multiple widgets, in this article, we'll go over ConsumerWidget() and Consumer().
Using a ConsumerWidget():
A consumer widget is a widget that we can use in place of our Stateful/Stateless widget. It gives us the ability to read/change the states of providers & also listen to them.
Dart
import 'Providers/providers.dart' as providers.dart; //for easy access
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
// Listens to the value exposed by userNameProvider
// ".state" method lets us get the state easily & directly
String name = watch(providers.userNameProvider).state;
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Center(child: const Text('GFG :) '))),
body: Center(
// displaying the value
child: Text('$name'),
),
),
);
}
}
Output:
Well and good. So now, whenever the value of userNameProvider changes, the Text() will be updated accordingly. But how are we gonna update the value of userNameProvider?
5. Updating/Changing the value of a StateProvider:
To change the value of StateProvider, we need a StateController. So let’s create it.
First, let’s remove the variable “name”. ̶
S̶t̶r̶i̶n̶g̶ ̶n̶a̶m̶e̶ ̶=̶ ̶w̶a̶t̶c̶h̶(̶p̶r̶o̶v̶i̶d̶e̶r̶s̶.̶u̶s̶e̶r̶N̶a̶m̶e̶P̶r̶o̶v̶i̶d̶e̶r̶)̶.̶s̶t̶a̶t̶e̶;̶
//removed as with it we can only listen/get the state not change/mutate the state.
//let's use this StateController instead.
StateController<String> nameController = watch(providers.userNameProvider);
// now we can get / set the state using name.state.
For the sake of simplicity, let’s use a FloatingActionButton, and using it let’s try to mutate(change) the state of userNameProvider.
Dart
// for easy access
import 'Providers/providers.dart' as providers.dart;
class Home extends ConsumerWidget {
int n = 0;
@override
Widget build(BuildContext context, ScopedReader watch) {
// Listens to the value exposed by userNameProvider
StateController<String> nameController = watch(providers.userNameProvider);
// ".state" method lets us get the state easily & directly
return MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
n++;
nameController.state =
// now we can set the state using the state setter.
"New Name $n";
},
),
appBar: AppBar(title: Center(child: const Text('GFG :) '))),
body: Center(
child: Text(
// displaying the value
'${nameController.state}',
),
),
),
);
}
}
Output:
Now, when we press on the FloatingActionButton(), we’ll see that the name changes every time with a different number. So that’s it. Using this information we can easily use StateProviders, mutate their values and listen to it.
Now, Let's go over asynchronous providers.
1. FutureProvider:
When we work with asynchronous code we often use some Future based APIs, let’s look into how you can handle Future events using Riverpod. To demonstrate, we will create a method which returns a Future.
Dart
class FutureClass {
// in production, the below method could be any network call
Future<int> getData(String para) async {
await Future.delayed(Duration(seconds: 5));
return 25;
}
}
Creating a provider for FutureClass:
final futureClass = Provider((ref) => FutureClass());
Now, let's create our FutureProvider,
Dart
final response = FutureProvider<int>((ref) async {
final client = ref.read(futureClass);
return client.getData('some text as a parameter');
});
Now, we can use ConsumerWidget to listen to the state changes and update our UI accordingly.
Consuming a FutureProvider:
Apart from the ConsumerWidget(), we also have a Consumer() widget. The difference is that while ConsumerWidget rebuilds the entire screen when state changes, Consumer() rebuilds only it's child, making sure we don't run into performance issues.
Let's use Consumer() widget to watch(listen) for state changes.
Dart
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("FutureProvider Demo"),),
body: Center(
child: Column(
children: [
Consumer(
builder: (context, watch, child) {
final futureData = watch(response);
},
),
],
),
),
);
}
}
As a Future can have 3 states, i.e, completed, in progress & error, we have the ability to handle those states separately using the .map function.