
If you’ve been at least a bit active when it comes to Flutter packages in the last year or so, you’ve surely heard about Riverpod, a reactive caching and data-binding, or as some would say, state management package that is sort of an upgrade of the beloved Provider. I actually covered it with a tutorial quite some time ago when its API was still unstable.
Riverpod has come a long way since then - it’s much more mature, helpful, and versatile. All these changes naturally mean that it’s time for a new tutorial to prepare you to fully utilize the power of Riverpod 2.0 and, most likely, also its upcoming versions.
The purpose of Riverpod has a lot in common with classes and packages like InheritedWidget
, Provider, get_it, and partly GetX. That is, to allow you to access objects across different parts of your app without passing all kinds of callbacks and objects as constructor parameters to the Widgets.
So what sets it apart from all of these other options offered to you from each side? It's the fact that it combines ease of use, clean coding practices, complete independence from Flutter (great for testing!), compile-time safety (as opposed to dealing with run-time errors), and performance optimization in one package. To achieve all this, Riverpod has a unique approach to how you declare the objects you want to provide around your app.
The version of the package we're using is 2.0.0-dev.5, so make sure to use at least that version in your pubspec. Everything we write in this tutorial will be valid once the stable version is released too.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.0.0-dev.5
Providers
Let’s first deal with the simplest possible example and say that you want a String
to be accessible throughout your app.
main.dart
// Provider declaration is top-level (global)
final myStringProvider = Provider((ref) => 'Hello world!');
If we break down the line of code above, you’ve just declared a provider under the name myStringProvider
, which will provide the “Hello world!” String
wherever you need. This is the most basic type of a Provider
that simply exposes a read-only value. We’ll take a look at the other more advanced types of providers shortly. The ref
parameter is of type ProviderReference
and it’s used, among other things, to interact with other providers - we’ll explore the ref
parameter later on as well.
This provider declaration is highly similar to declaring a class. A class declaration is accessible globally, but once you instantiate an object, it's no longer global, which tremendously helps with the app's maintainability and makes testing possible since hard-coded use of globals doesn't allow mocking.
main.dart
class MyClass {
int myField;
MyClass(this.myField);
}
// The object has to be passed into the function.
// We can't access it globally.
void myFunction(MyClass object) {
object.myField = 5;
}
In much the same way, a provider declaration is global, but the actual state it provides is not global. It is instead stored and managed in a widget called ProviderScope
, at which we'll take a closer look soon, and this keeps our app maintainable and testable. Essentially, we could say that we get all the benefits of global variables without any of their drawbacks. Yes, sometimes things that sound too good to be true are indeed true.
Riverpod & Widget Tree

Let's now look at a more real-world example - a counter app! Yes, precisely that counter app that you're so fed up with but don't despair because you will learn something new this time. I promise!
Here's the end result:
Naturally, we’re going to use Riverpod for the state management instead of the classic StatefulWidget
. By now, you’ve seen the most basic Provider
class that provides read-only data. Since we want to increment the counter when the user presses a button, we obviously need to write to the data too. The simplest way to achieve this is with a StateProvider
.
main.dart
final counterProvider = StateProvider((ref) => 0);
void main() => runApp(MyApp());
...
ChangeNotifier
, Bloc
, Cubit
or anything else you want in conjunction with Riverpod.Oh, and what about the single ProviderScope
widget I’ve mentioned earlier? That just simply needs to wrap the whole app widget in the main
method.
main.dart
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter App',
home: const HomePage(),
);
}
}
We’re also adding a little twist to this app by not doing the counting right in the “home” route. Instead, we’ll make the user first navigate to the CounterPage
widget from the HomePage
, so the HomePage
will contain only one button.
main.dart
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Center(
child: ElevatedButton(
child: const Text('Go to Counter Page'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: ((context) => const CounterPage()),
),
);
},
),
),
);
}
}
As you can see, we haven’t used the counterProvider
anywhere thus far. We made it possible to be used by declaring the provider itself and setting up the ProviderScope
but if we ran the app right now, no actual counterProvider
would ever be created, let alone utilized and incremented. Let’s change that in the CounterPage
.
main.dart
// ConsumerWidget is like a StatelessWidget
// but with a WidgetRef parameter added in the build method.
class CounterPage extends ConsumerWidget {
const CounterPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// Using the WidgetRef to get the counter int from the counterProvider.
// The watch method makes the widget rebuild whenever the int changes value.
// - something like setState() but automatic
final int counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Text(
counter.toString(),
style: Theme.of(context).textTheme.displayMedium,
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
// Using the WidgetRef to read() the counterProvider just one time.
// - unlike watch(), this will never rebuild the widget automatically
// We don't want to get the int but the actual StateNotifier, hence we access it.
// StateNotifier exposes the int which we can then mutate (in our case increment).
ref.read(counterProvider.notifier).state++;
},
),
);
}
}
Not Preserving the State
The app now successfully increments the counter and even preserves the state, in this case, the one counter integer for the lifetime of the app session. But what if we want the user to always start counting from zero once the CounterPage
is opened (even reopened after being closed previously)?
That’s simple! The only thing we need to add is the autoDispose
modifier to the counterProvider
at the very top of the file.
main.dart
final counterProvider = StateProvider.autoDispose((ref) => 0);
How does this work? Why is the counterProvider
’s state now disposed after the user closed and disposed the CounterPage
?
Riverpod knows which widgets use the individual providers. After all, we are continuously subscribed to the counterProvider
in the CounterPage
by calling the watch
method. In our case, this also happens to be the only subscription to the counterProvider
in the entire app, so once that subscription no longer exists because the CounterPage
widget has been closed and disposed, Riverpod knows that the counterProvider
’s state can also be disposed.
And by what kind of magic is that done? Well, the CounterPage
is a subclass of ConsumerWidget
that comes from the Riverpod package too, so all the necessary code responsible for disposing of provider state is hidden in there.
Resetting the State Manually
Disposing of the state and thus releasing resources when the provider is no longer in use is one thing, but you may sometimes want to manually reset the state, for example, with a button. This is very easy with ref.invalidate
.
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
actions: [
IconButton(
onPressed: () {
ref.invalidate(counterProvider);
},
icon: const Icon(Icons.refresh),
),
],
),
...
ref.refresh
instead, which will return the newly reset state - in our case, that would be the integer 0.Performing Actions Based on the State

By now we’ve seen that watch
is used within the build
method for getting the provider state and rebuilding a widget with it, while read
is for doing just one-off actions with the provider outside of the build
method - usually in button onPressed
or similar callbacks.
But how can we, for example, navigate, show snackbars, alerts, or do any kind of other action whenever the state of the provider changes to the desired value? We can’t use the state we get from watch
and just do these actions directly in the build
method because we’ll get the infamous “setState() or markNeedsBuild() called during build” error thrown in our face. Instead, we need to use the listen
method.
Let's say that we consider the number 5 to be dangerously large and want to show a dialog warning the user about it like this:
The following ref.listen
call is what needs to go into the CounterPage
.
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final int counter = ref.watch(counterProvider);
ref.listen<int>(
counterProvider,
// "next" is referring to the new state.
// The "previous" state is sometimes useful for logic in the callback.
(previous, next) {
if (next >= 5) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Warning'),
content:
Text('Counter dangerously high. Consider resetting it.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'),
)
],
);
},
);
}
},
);
...
}
Inter-Provider Dependencies
Apps aren't always as simple as having a single provider for the counter which means you are usually going to have multiple providers at once. As if that wasn't enough, the objects that the providers provide are often going to depend on one another. For example, you may have a Cubit
or a ChangeNotifier
that depends on a Repository
from which it gets the data.
Providers make dealing with dependencies between classes totally simple. In fact, you've already seen the syntax that's used for it in this very article, and if you're new to Riverpod, you may not have even noticed.
Let's say that we want to upgrade our familiar counter app to be a lot fancier. Everybody knows that keeping all of your state local is lame, and the cool kids put everything on serverless servers™, and we surely don't want to fall behind! We will create the ultimate counter app that gets its counter integer value through a WebSocket. Sort of...
To keep the app fit for a tutorial, we'll just fake the WebSocket and simply return a locally generated Stream
of integers that are incremented every half a second. We'll also utilize an abstract class to serve as an interface so that the code can be easily swapped for a real implementation or tested.
main.dart
abstract class WebsocketClient {
Stream<int> getCounterStream();
}
class FakeWebsocketClient implements WebsocketClient {
@override
Stream<int> getCounterStream() async* {
int i = 0;
while (true) {
await Future.delayed(const Duration(milliseconds: 500));
yield i++;
}
}
}
Providing the FakeWebsocketClient
object is very straightforward:
main.dart
final websocketClientProvider = Provider<WebsocketClient>(
(ref) {
return FakeWebsocketClient();
},
);
This is not the end provider we want the UI to have access to though. We need to call the getCounterStream
method on the WebsocketClient
to get the counter Stream
we’re after all along.
Naturally, we’re going to create a new counterProvider
of type StreamProvider
. In order to call the getCounterStream
method though, we first need to have the WebsocketClient
object that is provided by the provider we created in the code snippet above.
StreamProvider
is just another type of provider that has its declaration syntax identical to all the other providers we've already seen. Obviously, the object you provide with it must be of type Stream
. This allows for some nice syntax when consuming the data from the widget tree - no more clunky StreamBuilder
s!To get access to the WebsocketClient
, we can simply read the websocketClientProvider
using the ref
parameter that’s included in every single provider’s creation callback.
main.dart
final counterProvider = StreamProvider<int>((ref) {
final wsClient = ref.watch(websocketClientProvider);
return wsClient.getCounterStream();
});
The ref.watch
call looks familiar, doesn’t it? Of course it does - it’s exactly the same thing you do within the widget tree with the minor difference being that here the ref
parameter is not of type WidgetRef
but rather StreamProviderRef<int>
.
ref
parameter in the callback allows you to do everything that a WidgetRef
allows you to do (watch
, read
, listen
, invalidate
...) and more, such as adding different callbacks.Our new and fancy CounterPage
with the counter state management outsourced to a fake serverless server will now look as follows. Notice the ease with which we consume the Stream
within the UI. We actually don’t even see it since it gets transformed to an AsyncValue
by Riverpod.
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// AsyncValue is a union of 3 cases - data, error and loading
final AsyncValue<int> counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Text(
counter
.when(
data: (int value) => value,
error: (Object e, _) => e,
// While we're waiting for the first counter value to arrive
// we want the text to display zero.
loading: () => 0,
)
.toString(),
style: Theme.of(context).textTheme.displayMedium,
),
),
);
}
}
Passing an Argument to a Provider

The current implementation of the counter client always starts from zero but our customers started giving us one-star reviews saying that they need to be able to modify the starting value of the counter. So we naturally implement their feature request as follows:
main.dart
abstract class WebsocketClient {
Stream<int> getCounterStream([int start]);
}
class FakeWebsocketClient implements WebsocketClient {
@override
Stream<int> getCounterStream([int start = 0]) async* {
int i = start;
while (true) {
await Future.delayed(const Duration(milliseconds: 500));
yield i++;
}
}
}
That would be it for the WebsocketClient
and its “fake” implementation but we also need to somehow pass the starting value to the counterProvider
since that is what the widgets actually use to get to the counter Stream
.
Until now, we’ve never passed anything to a provider. We could read
, watch
or listen
to it but it always contained everything needed and didn’t get anything passed from the outside. Passing arguments to providers is luckily very simple thanks to the family
modifier.
main.dart
// The "family" modifier's first type argument is the type of the provider
// and the second type argument is the type that's passed in.
final counterProvider = StreamProvider.family<int, int>((ref, start) {
final wsClient = ref.watch(websocketClientProvider);
return wsClient.getCounterStream(start);
});
family
modifier can be combined with the autoDispose
modifier like StreamProvider.autoDispose.family<int, int>
The family
modifier makes our counterProvider
to be a callable class, so to pass in a start value from the CounterPage
, we can simply do this:
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Just hardcoding the start value 5 for simplicity
final AsyncValue<int> counter = ref.watch(counterProvider(5));
...
}
}
Conclusion
And just like that, you've learned how to use the powerful Riverpod package by building and expanding upon the simple counter app. You're now ready to employ all this knowledge in your own complex and cool apps that Riverpod makes hassle-free to build and easy to maintain - at least when it comes to getting objects around the app 😉
Thank you for the good information.
My name is PONSUKE and I am a beginner.
I have a question about Inter-Provider Dependencies in this guide.
Why do I need to create a websocketClientProvider?
Why not instantiate wsClient with counterProvider as follows?
final counterProvider = StreamProvider((ref) {
FakeWebsocketClient wsClient = FakeWebsocketClient();
return wsClient.getCounterStream();
});
I would like to know if there are any problems if I do it this way.
Thank you
You can and may call the implementation class directly. However, once you want to change the “fake” client with “real” client, you have to modify counterProvider itself instead of simply change the implementation supplied by dependency injection. This also make things complicated when using tests.
Thank you.
It was a good awareness.
I will try to learn more about dependency injection and Riverpod.
Thanks for the guide. Will you create a series of Riverpod + DDD architecture + Test?
I had another question, which state management framework that you use for customer production? Is it BLOC or Riverpod now.
At the beginning of the article, “ref.read(counterProvider.notifier).state++;” is used. But what is the difference between that and “ref.read(counterProvider.state).state++;” where counterProvider.state is used instead of counterProvider.notifier?
Great information well written – although I’d like to have seen more complex (e.g. CRUD) examples handled…
But that font – using the weird ‘Chips’ for all keywords made it very hard to read, for me!
Test
Just a nitpick about wording because you even have it in bold letter at the beginning…
‘Declare’ means that you introduce a name. You are saying there is some function “foo” that takes an int and returns an int something like
“`
int foo(int);
“`
But what you are doing at the beginning that you ‘define’ a provider because you are not only introducing a name that can be used from there on but you also “defined” what it is doing.
This doesn’t seem to be about Riverpod 2. There is no mention of the new @riverpod notation.