Flutter tutorials and courses by Andrea Bizzotto

Flutter State Management with Riverpod: The Essential Guide

Riverpod is a popular Flutter state management library that shares many of the advantages of Provider and brings many additional benefits.

According to the official documentation:

Riverpod is a complete rewrite of the Provider package to make improvements that would be otherwise impossible.

I won't lie: Riverpod comes with a bit of a learning curve.

But it is a very powerful state management library that allows you to:

  • easily create, access, and combine providers with minimal boilerplate code
  • write testable code and keep your logic outside the widget tree
  • catch programming errors at compile-time rather than at runtime

This reference guide covers all the essential concepts so that you can start using Riverpod in your own apps.

We'll cover the basics with some simple examples and explore some practical use cases that you may encounter in real-world apps.

Note: This guide is based on Riverpod version 1.0.0-dev.7.

Table of Contents

Why use Riverpod for Flutter state management?

To understand why we even need Riverpod, let's look at some of the most common problems with Provider.

By design, Provider is an improvement over InheritedWidget and it depends on the widget tree.

This leads to some drawbacks:

Provider Drawback #1: Combining Providers is very verbose

When a provider depends on another provider we often end up with code that looks like this:

Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); final sharedPreferences = await SharedPreferences.getInstance(); // group multiple providers together runApp(MultiProvider( providers: [ // a provider Provider<SharedPreferences>(create: (_) => sharedPreferences), // another provider that depends on the previous one ChangeNotifierProxyProvider<SharedPreferences, OnboardingViewModel>( create: (_) => OnboardingViewModel(sharedPreferences), update: (context, sharedPreferences, _) => OnboardingViewModel(sharedPreferences), ), ], // a consumer that can access the provider above child: Consumer<OnboardingViewModel>( builder: (_, viewModel) => OnboardingPage(viewModel), ), )); }

In this example, we have an OnboardingPage widget that takes an OnboardingViewModel argument. But because OnboardingViewModel itself depends on SharedPreferences, we need:

  • a MultiProvider to group multiple providers together
  • a Provider<SharedPreferences> to make the SharedPreferences object available to descendant widgets
  • a ChangeNotifierProxyProvider to create the OnboardingViewModel that depends on SharedPreferences
  • a Consumer<OnboardingViewModel> to retrieve the OnboardingViewModel and pass it to the OnboardingPage

That's a lot of code for something that should be relatively simple. It would be better if all these provider dependencies could be specified outside the widget tree.

Provider Drawback #2: Getting Providers by type and runtime exceptions

Inside any widget class, we can access our providers by type with this syntax:

Provider.of<MyType>(context)

But if we're not careful we can end up with a ProviderNotFoundException at runtime:

Example: accessing Providers in the widget tree
Example: accessing Providers in the widget tree

This can become a problem with large apps and it would be much better if provider access could be resolved at compile-time.

In addition, if there are two or more ancestor providers of the same type, we can only access the one that is closest to our widget.


Riverpod is completely independent from the widget tree and it doesn't suffer from any of these drawbacks.

So let's see how to use it.

Riverpod Installation

The first step is to add the latest version of flutter_riverpod as a dependency to our pubspec.yaml file:

dependencies: flutter: sdk: flutter flutter_riverpod: ^1.0.0-dev.7

Note: If your application already uses flutter_hooks, you can install the hooks_riverpod package instead. This includes some extra features that make it easier to integrate Hooks with Riverpod. In this tutorial, we will focus on flutter_riverpod only for simplicity.

Top Tip: To more easily add Riverpod providers in your code, install the Flutter Riverpod Snippets VSCode extension.

ProviderScope

Once Riverpod is installed, we can wrap our root widget with a ProviderScope:

void main() { runApp(ProviderScope( child: MyApp(), )); }

ProviderScope is a widget that stores the state of all the providers we create.

Creating a Provider

Let's see how we can modify the sample counter app to use Riverpod.

In our main.dart we can replace the default MyHomePage widget with this:

class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text( 'Some text here 👍', style: Theme.of(context).textTheme.headline4, ), ), ); } }

And just above it we can add a global valueProvider:

final valueProvider = Provider<int>((ref) { return 36; });

Reading a Provider

But how can we read the provider value inside our widget?

Riverpod offers multiple ways of doing this:

Using a Consumer

We can wrap our Text widget with a Consumer:

class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( // 1. Add a Consumer child: Consumer( // 2. specify the builder and obtain a WidgetRef builder: (_, WidgetRef ref, __) { // 3. use ref.watch() to get the value of the provider final value = ref.watch(valueProvider); return Text( 'Value: $value', style: Theme.of(context).textTheme.headline4, ); }, ), ), ); } }

The Consumer's builder argument gives us a WidgetRef object that we can use to watch the value of the provider.

Riverpod lets us access providers by reference, not by type. This means that we can have as many providers as we want of the same type.

The code above works, but adding a parent Consumer to the Text widget is a bit too verbose.

To make life easier, Riverpod also offers a ConsumerWidget.

Using a ConsumerWidget

Here's how we can modify our example:

// 1. Widget class now extends [ConsumerWidget] class MyHomePage extends ConsumerWidget { @override // 2. build() method has an extra [WidgetRef] argument Widget build(BuildContext context, WidgetRef ref) { // 3. use ref.watch() to get the value of the provider final value = ref.watch(valueProvider); return Scaffold( body: Center( child: Text( 'Value: $value', style: Theme.of(context).textTheme.headline4, ), ), ); } }

By subclassing ConsumerWidget, our widget's build() method now has an extra WidgetRef argument, and we can use this to watch our provider. Less code, more win. 😎

Using ConsumerStatefulWidget & ConsumerState

ConsumerWidget is a good replacement for StatelessWidget and gives us a convenient way of accessing providers with minimal boilerplate code.

But what if we have a StatefulWidget instead?

Here's the same counter example:

// 1. Extend [ConsumerStatefulWidget] class MyHomePage extends ConsumerStatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } // 2. Extend [ConsumerState] class _MyHomePageState extends ConsumerState<MyHomePage> { @override void initState() { super.initState(); // 3. use ref.read() in the widget life-cycle methods final value = ref.read(valueProvider); print(value); } @override Widget build(BuildContext context) { // 3. use ref.watch() to get the value of the provider final value = ref.watch(valueProvider); return Scaffold( body: Center( child: Text( 'Value: $value', style: Theme.of(context).textTheme.headline4, ), ), ); } }

By subclassing from ConsumerStatefulWidget and ConsumerState, we can use ref.watch() in the build() method and read the provider value as needed.

And if we need to read the provider value in any of the other widget life-cycle methods, we can use ref.read().

Note how the build() method only gives us a BuildContext when we subclass from ConsumerState, but we can still access the ref object. This is because ConsumerState declares WidgetRef as a property, much like the Flutter State class declares BuildContext as a property that can be accessed directly inside all the widget life-cycle methods.

What is a WidgetRef?

As we have seen, we can watch a provider's value by using a "ref" object of type WidgetRef. This is available as an argument when we use Consumer or ConsumerWidget, and as a property when we subclass from ConsumerState.

The Riverpod documentation defines WidgetRef as an object that allows widgets to interact with providers.

To better understand what is the purpose of WidgetRef, let's compare it to BuildContext:

  • BuildContext lets us access ancestor widgets in the widget tree (such as Theme.of(context) and MediaQuery.of(context))
  • WidgetRef lets us access any provider inside our app

In other words, WidgetRef lets us access any provider we want. This is by design because all Riverpod providers are global.

As we will see, this has many advantages and lets us easily move any state management logic outside the widget tree.

Note: if we use ref.watch(), our widget becomes reactive and rebuild itself when the provider value changes.

So keep this in mind as we explore all the remaining providers.

StateProvider

So far we managed to read a provider's value inside a widget. But Provider itself doesn't give us any capability to change its value. For that we need to create a StateProvider:

final counterStateProvider = StateProvider<int>((ref) { return 0; });

Then, we can modify our widget code:

class MyHomePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // 1. watch the counterStateProvider final counter = ref.watch(counterStateProvider); return Scaffold( body: Center( child: Text( // 2. this time we read counter.state 'Value: ${counter.state}', style: Theme.of(context).textTheme.headline4, ), ), ); } }

To verify that our code works, we can add a floatingActionButton argument to our Scaffold:

floatingActionButton: FloatingActionButton( // access the provider via ref.read(), then increment its state. onPressed: () => ref.read(counterStateProvider).state++, child: Icon(Icons.add), ),

If we run the app now, we can press the FAB and see the updated counter value inside the Text widget.

Note: We should always use ref.read() rather than ref.watch() to access providers inside a callback. More on this below.

Listening to Provider State Changes

Sometimes we want to push a route, show a dialog, or a SnackBar when a provider state changes.

We can do this by calling ref.listen() inside our build() method:

Widget build(BuildContext context, WidgetRef ref) { ref.listen(counterStateProvider, (StateController<int> counterState) { // note: this callback executes when the provider value changes, // not when the build method is called ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Value is ${counterState.state}')), ); }); return Scaffold(...); }

In this case, the callback gives us a StateController<int> argument that represents the new state of our provider, and we can use it to show a SnackBar.

ref.listen() gives us a callback that executes when the provider value changes, not when the build() method is called. Hence we can use it to run any asynchronous code (such as a network request), just like we do with regular button callbacks.

StateNotifierProvider

Provider and StateProvider are sufficient for simple use cases like the counter example.

But in more complex apps we often need to store some state along with some business logic outside our widget classes.

The state_notifier package offers a good solution for this. If you're not familiar with StateNotifier, you can read my tutorial about Freezed & StateNotifier.

Since flutter_riverpod depends on the state_notifier package, we can use it without adding it explicitly in our pubspec.yaml file.

As an example, let's see how to create a clock using StateNotifier and the Timer class:

import 'dart:async'; class Clock extends StateNotifier<DateTime> { // 1. initialize with current time Clock() : super(DateTime.now()) { // 2. create a timer that fires every second _timer = Timer.periodic(Duration(seconds: 1), (_) { // 3. update the state with the current time state = DateTime.now(); }); } late final Timer _timer; // 4. cancel the timer when finished @override void dispose() { _timer.cancel(); super.dispose(); } }

This class sets the initial state in the constructor (by calling super(DateTime.now())), and updates the state every second using a periodic timer.

Once we have this, we can create a new provider:

final clockProvider = StateNotifierProvider<Clock, DateTime>((ref) { return Clock(); });

Then, we can add intl as a dependency to our pubspec.yaml and use it to format the time in our widget class:

import 'package:intl/intl.dart'; class MyHomePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // this line is used to watch the provider's *state* // to get an instance of the clock itself, // call `ref.watch(clockProvider.notifier)` final currentTime = ref.watch(clockProvider); // format the time as `hh:mm:ss` final timeFormatted = DateFormat.Hms().format(currentTime); return Scaffold( body: Center( child: Text( timeFormatted, style: Theme.of(context).textTheme.headline4, ), ), ); } }

Since the clock state updates every second and we're using ref.watch(clockProvider), our widget will also rebuild every second and show the updated time.

Note: ref.watch(clockProvider) returns the provider's state. To get access to the underlying state notifier object, call ref.watch(clockProvider.notifier) instead.

This example is for illustration purposes only. The correct way of implementing a clock widget is to use a Ticker, as explained here: Flutter Timer vs Ticker: A Case Study

Use StateNotifier for your Business Logic

The clock example above is a minimal use case of StateNotifier. In general, you should always think of StateNotifier as the place where your business logic goes:

Interaction between widget and model classes

When you setup things this way, your widgets can:

  • watch the model's state and rebuild when it changes.
  • call methods in your model classes (using ref.read(provider).someMethod()), which in turn can update the state and interact with external services if needed.

ChangeNotifierProvider

In addition to StateNotifierProvider, Riverpod also has a ChangeNotifierProvider that works in a very similar way:

  • you can declare a model class with ChangeNotifier as a mixin.
  • you can create a ChangeNotifierProvider that returns that model and access it inside your widgets with ref.watch().

StateNotifierProvider and ChangeNotifierProvider are very similar, but ChangeNotifier has various drawbacks that I listed in my tutorial about Freezed & StateNotifier. So I recommend using StateNotifier and StateNotifierProvider instead.

FutureProvider & StreamProvider

When we work with asynchronous code we often use some Future or Stream-based APIs.

By using Flutter's FutureBuilder and StreamBuilder widgets we can rebuild our UI when some asynchronous data changes.

Though these widgets are quite clunky to use.

For example, when we use StreamBuilder we need to perform multiple checks on the builder's snapshot to map our data to the UI:

final stream = Stream.fromIterable([21, 42]); StreamBuilder<int>( stream: stream, builder: (context, snapshot) { // spaghetti code anyone? if (snapshot.connectionState == ConnectionState.active) { if (snapshot.hasData) { return MyWidget(snapshot.data); // data } else if (snapshot.hasError) { return MyErrorWidget(snapshot.error); // error } else { return Text('No data'); // no data } } else { return CircularProgressIndicator(); // loading } } )

Luckily, Riverpod comes to the rescue with FutureProvider and StreamProvider. 💪

Here's how we can declare them:

final futureProvider = FutureProvider<int>((ref) { return Future.value(36); }); final streamProvider = StreamProvider<int>((ref) { return Stream.fromIterable([36, 72]); });

And here's how to use them:

class MyHomePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final streamAsyncValue = ref.watch(streamProvider); // final futureAsyncValue = ref.watch(futureProvider); // same syntax return Scaffold( body: Center( child: streamAsyncValue.when( data: (data) => Text('Value: $data'), loading: () => CircularProgressIndicator(), error: (e, st) => Text('Error: $e'), ), ), ); } }

When we watch a FutureProvider or StreamProvider we get an AsyncValue<T>, which is a type that comes from the Freezed package.

And then we can use the when() method to map the data, loading, and error states to different widgets:

asyncValue.when( data: (data) => Text('Value: $data'), loading: () => CircularProgressIndicator(), error: (e, st) => Text('Error: $e'), )

Much easier than when we use StreamBuilder. 🙏

Advanced Riverpod Features

We have completed our overview of the core features of Riverpod. And you can already start using them to get some practice.

But Riverpod also has some advanced features that make it easier to handle more complex scenarios.

So let's take a deeper dive and learn about them.

The autoDispose modifier

If we're working with FutureProvider or StreamProvider, we'll want to dispose of any listeners when our provider is no longer in use.

This is easily done by adding an .autoDispose modifier to our provider:

final streamProvider = StreamProvider.autoDispose<int>((ref) { return Stream.fromIterable([36, 72]); });

If we're working with streams that come from Firebase, this is all we need.

Another use case is when we're using FutureProvider as a wrapper for an HTTP request that fires when the user enters a new screen. And we want to cancel the HTTP request if the user leaves the screen before the request is completed.

In this scenario, we can use ref.onDispose() to perform some custom cancellation logic:

final myProvider = FutureProvider.autoDispose((ref) async { // An object from package:dio that allows cancelling http requests final cancelToken = CancelToken(); // When the provider is destroyed, cancel the http request ref.onDispose(() => cancelToken.cancel()); // Fetch our data and pass our `cancelToken` for cancellation to work final response = await dio.get('path', cancelToken: cancelToken); // If the request completed successfully, keep the state ref.maintainState = true; return response; });

We can also set the maintainState flag to preserve the state so that the request does not fire again if the user leaves and re-enters the same screen.

See the autoDispose documentation for more details on this.

The family modifier

.family is another modifier that we can use pass additional values to our providers.

final streamProvider = StreamProvider.autoDispose.family<int, int>((ref, offset) { return Stream.fromIterable([36 + offset, 72 + offset]); });

This works by adding a second type annotation and an additional argument that we can use inside the provider body.

Then, we can just pass the value we want to the provider inside ref.watch():

final streamAsyncValue = ref.watch(streamProvider(10));

The family modifier has many practical applications.

For example, consider this code:

// some model class class Item { Item(this.id, this.name); final String id; final String name; /* many other properties here */ } class Database { Stream<Item> itemStream(String itemId) { /* return a single item matching the given id */ } }

Here we have a Database class that has access to a collection of items, and we can use the itemStream() method to get a single item by id.

We can then create some providers:

final databaseProvider = Provider<Database>((ref) => Database()); // 1. add an itemId argument with a family modifier final itemStreamProvider = StreamProvider.autoDispose.family<Item, String>((ref, itemId) { // 2. retrieve the database with ref.watch() final database = ref.watch(databaseProvider); // 3. pass the itemId to the database method and return the output stream return database.itemStream(itemId); });

In this case, the itemStreamProvider can get the database via ref.watch(), and uses the itemId as an argument for the itemStream() method.

Then, we can create an ItemWidget that consumes this data:

class ItemWidget extends ConsumerWidget { const ItemWidget({required this.itemId}); final String itemId; @override Widget build(BuildContext context, WidgetRef ref) { final itemAsyncValue = ref.watch(itemStreamProvider(itemId)); return itemAsyncValue.when( data: (job) => Text(job.name), loading: () => Container(), error: (_, __) => Container(), ); } }

A common use case for this is when the user selects an item inside a ListView, and we want to reveal a detail screen that needs to show additional information for that item.

Note: in some cases, you may need to pass more than one value to a family. This is not supported by Riverpod. To work around this you can create a custom data class to hold all the properties you need. Alternatively you can pass a map of key-value pairs, though this is not type-safe.

Dependency Overrides with Riverpod

Sometimes we want to create a Provider to store a value or object that is not immediately available.

For example, we can only get a SharedPreferences instance with a Future-based API:

final sharedPreferences = await SharedPreferences.getInstance();

But we can't return this inside a synchronous Provider:

final sharedPreferencesProvider = Provider<SharedPreferences>((ref) { return SharedPreferences.getInstance(); // The return type Future<SharedPreferences> isn't a 'SharedPreferences', // as required by the closure's context. });

Instead, we have to initialize this provider by throwing an UnimplementedError:

final sharedPreferencesProvider = Provider<SharedPreferences>((ref) { throw UnimplementedError(); });

And when the object we need is available, we can set a dependency override for our provider inside the ProviderScope widget:

Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); final sharedPreferences = await SharedPreferences.getInstance(); runApp(ProviderScope( overrides: [ // override the previous value with the new object sharedPreferencesProvider.overrideWithValue(sharedPreferences), ], child: MyApp(), )); }

And since we initialize sharedPreferences before calling runApp(), we can watch the sharedPreferencesProvider object anywhere in our app without using any Future-based APIs.

This example used the ProviderScope at the root of the widget tree, but we can also create nested ProviderScope widgets if needed. More on this below.

Combining Providers with Riverpod

Providers can depend on other providers.

Consider this example:

final onboardingViewModelProvider = StateNotifierProvider<OnboardingViewModel>((ref) { final sharedPreferences = ref.watch(sharedPreferencesProvider); return OnboardingViewModel(sharedPreferences); });

Here we define an onboardingViewModelProvider provider that depends on the sharedPreferencesProvider we created above.

By using ref.watch() we ensure that the provider is updated when the parent provider changes. As a result, any dependent widgets are rebuilt.

We can then pass the sharedPreferences object as a constructor argument to a view model class based on StateNotifier:

class OnboardingViewModel extends StateNotifier<bool> { OnboardingViewModel(this.sharedPreferences) : super(sharedPreferences.getBool('onboardingComplete') ?? false); final SharedPreferences sharedPreferences; // OnboardingViewModel methods here }

With Riverpod, we can declare providers that contain complex logic or depend on other providers, all outside the widget tree. This is a great advantage over the Provider package and makes it easier to write widgets that only contain UI code.

Scoping Providers

With Riverpod, we can scope providers so that they behave differently for a specific part of the application.

An example of this is when we have a ListView that shows a list of products, and each item needs to know the correct product id or index:

class ProductList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( itemBuilder: (_, index) => ProductItem(index: index), ); } }

In the code above, we pass the builder's index as a constructor argument to the ProductItem widget:

class ProductItem extends StatelessWidget { const ProductItem({Key? key, required this.index}) : super(key: key); final int index; @override Widget build(BuildContext context) { // do something with the index } }

This works but if the ListView rebuilds, all of its children will rebuild too.


As an alternative, we can override the provider value inside a nested ProviderScope:

// 1. Declare a ScopedProvider final currentProductIndex = Provider<int>((_) => throw UnimplementedError()); class ProductList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder(itemBuilder: (context, index) { // 2. Add a parent ProviderScope return ProviderScope( overrides: [ // 3. Add a dependency override on the index currentProductIndex.overrideWithValue(index), ], // 4. return a **const** ProductItem with no constructor arguments child: const ProductItem(), ); }); } } class ProductItem extends ConsumerWidget { const ProductItem(); @override Widget build(BuildContext context, WidgetRef ref) { // 5. Access the index via WidgetRef final index = ref.watch(currentProductIndex); // do something with the index } }

In this case:

  • we create a Provider that throws UnimplementedError by default.
  • we override its value by adding a parent ProviderScope to the ProductItem widget.
  • we watch the index inside the ProductItem's build() method.

This is better for performance because we can create ProductItem() as a const widget in the ListView.builder. This means that even if the ListView rebuilds, our ProductItem will not rebuild unless its index has changed.

Filtering widget rebuilds with "select"

Sometimes you have a model class with multiple properties and you want to rebuild a widget only when a specific property changes.

For example, consider this Connection class, along with a provider and a widget class that reads it:

class Connection { Connection({this.bytesSent = 0, this.bytesReceived = 0}); final int bytesSent; final int bytesReceived; } // Using [StateProvider] for simplicity. This could be a [FutureProvider] or [StreamProvider] final connectionProvider = StateProvider<Connection>((ref) { return Connection(); }); class BytesReceivedText extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // rebuild when bytesSent OR bytesReceived changes final counter = ref.watch(connectionProvider).state; return Text('${counter.bytesReceived}'); } }

If we use ref.watch(), our widget will (incorrectly) rebuild when the bytesSent value changes.

Instead, we can use select() to only listen to a specific property:

class BytesReceivedText extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // only rebuild when bytesReceived changes final bytesReceived = ref.watch(connectionProvider.select( (connection) => connection.state.bytesReceived )); return Text('$bytesReceived'); } }

Then, whenever the Connection changes, Riverpod will compare the value we're returning (connection.state.bytesReceived) and only rebuild the widget if it's different from the previous one.

The select() method is available on all Riverpod providers and can be used whenever we call ref.watch() or ref.listen(). For more info, see Using "select" to filter rebuilds in the Riverpod docs.

When to use ref.watch() vs ref.read() with Riverpod

In the examples above, we have used ref.watch() every time we needed to get the value of a provider inside a build() method.

This is the right thing to do because if the provider value changes, we will rebuild the widgets that depend on it.

Similarly, we should use ref.watch() when combining multiple providers:

final onboardingViewModelProvider = StateNotifierProvider<OnboardingViewModel>((ref) { // use ref.watch() inside a provider's body final sharedPreferencesService = ref.watch(sharedPreferencesProvider); return OnboardingViewModel(sharedPreferencesService); });

But there are cases when watch() shouldn't be used.

For example, inside the onPressed callback of a button we should use ref.read() instead:

void someButtonCallback(WidgetRef ref) { // callbacks do not execute when the build() method is called // so we use ref.read(), not ref.watch() final sharedPreferences = ref.read(sharedPreferencesProvider); sharedPreferences.setBool('onboardingCompleted', true); } // call like this inside a button: ElevatedButton( onPressed: () => someButtonCallback(ref), child: ... )

As a rule of thumb:

  • Use ref.watch() inside the build() methods, builder callbacks, and the body of your providers.
  • Use ref.read() inside button callbacks or callback handlers.

As I have explained in my Flutter state management basics, UI is a function of state:

UI = f(state)

So we should rebuild the UI when the state changes, and ref.watch() gives us an elegant way of doing this.

On the other hand, widgets can use events or method calls to notify the relevant model classes or repositories when a user interaction has occurred. For that, we can use ref.read().

For more details on how to use WidgetRef, read the Reading a provider page on the official Riverpod documentation.

Testing with Riverpod

As we've seen, Riverpod providers are global but their state isn't.

The state of a provider is stored inside a ProviderContainer, an object that is implicitly created by ProviderScope.

This means that separate widget tests will never share any state. So there is no need for setUp() and tearDown() methods.

For example, here is a simple counter application that uses a StateProvider to store the counter value:

final counterProvider = StateProvider((ref) => 0); void main() { runApp(ProviderScope(child: MyApp())); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Consumer(builder: (_, ref, __) { final counter = ref.watch(counterProvider); return ElevatedButton( onPressed: () => ref.read(counterProvider).state++, child: Text('${counter.state}'), ); }), ); } }

The code above uses an ElevatedButton to show the counter value and increment it via the onPressed callback.

When writing widget tests, all we need is this:

await tester.pumpWidget(ProviderScope(child: MyApp()));

With this setup, multiple tests don't share any state because each test has a different ProviderScope:

void main() { testWidgets('incrementing the state updates the UI', (tester) async { await tester.pumpWidget(ProviderScope(child: MyApp())); // The default value is `0`, as declared in our provider expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Increment the state and re-render await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // The state have properly incremented expect(find.text('1'), findsOneWidget); expect(find.text('0'), findsNothing); }); testWidgets('the counter state is not shared between tests', (tester) async { await tester.pumpWidget(ProviderScope(child: MyApp())); // The state is `0` once again, with no tearDown/setUp needed expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); }); }

How to mock and override dependencies in tests

Many applications need to call REST APIs or communicate with external services.

For example, here is a MoviesRepository that we can use to get a list of favourite movies:

class MoviesRepository { Future<List<Movie>> favouriteMovies() async { // get data from the network or local database } }

And we can create create a moviesProvider to get the data we need:

final moviesRepositoryProvider = Provider((ref) => MoviesRepository()); final moviesProvider = FutureProvider<List<Movie>>((ref) { // access the provider above final repository = ref.watch(moviesRepositoryProvider); // use it to return a Future return repository.favouriteMovies(); });

When writing widget tests, we want to replace our MoviesRepository with a mock that returns a canned response rather than making a network call.

As we have seen, we can use dependency overrides to change the behaviour of a provider by replacing it with a different implementation.

So we can implement a MockMoviesRepository:

class MockMoviesRepository implements MoviesRepository { @override Future<List<Movie>> favouriteMovies() { return Future.value([ Movie(id: 1, title: 'Rick & Morty', posterUrl: 'https://nnbd.me/1.png'), Movie(id: 2, title: 'Seinfeld', posterUrl: 'https://nnbd.me/2.png'), ]); } }

And in our widget tests we can override the repository provider:

void main() { testWidgets('Override moviesRepositoryProvider', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ moviesRepositoryProvider .overrideWithProvider(Provider((ref) => MockMoviesRepository())) ], child: MoviesApp(), ), ); }); }

As a result, the MoviesApp() widget will load the data from the MockMoviesRepository when the tests run.

This setup still works if you use mocktail in your tests. You can stub your mock methods to return values or throw exceptions, and verify that certain methods are called on your mocks.

For more details about testing with Riverpod, see the official documentation.

Logging with ProviderObserver

Monitoring state changes is very useful in many apps.

And Riverpod includes a ProviderObserver that we can subclass to implement a Logger:

class Logger extends ProviderObserver { @override void didUpdateProvider( ProviderBase provider, Object? previousValue, Object? newValue, ProviderContainer container, ) { print('[${provider.name ?? provider.runtimeType}] value: $newValue'); } }

This gives us access to both the previous and new value.

We can enable logging for the entire app by adding the Logger to the list of observers inside the ProviderScope:

void main() { runApp( ProviderScope(observers: [Logger()], child: MyApp()), ); }

To improve the output of the logger, we can add a name to our providers:

final counterStateProvider = StateProvider<int>((ref) { return 0; }, name: 'counter');

And if needed, we can tweak the output of the logger based on the values being observed:

class Logger extends ProviderObserver { @override void didUpdateProvider( ProviderBase provider, Object? previousValue, Object? newValue, ProviderContainer container, ) { if (newValue is StateController<int>) { print( '[${provider.name ?? provider.runtimeType}] value: ${newValue.state}'); } } }

ProviderObserver is versatile and we can configure our logger to only log values that match a certain type or provider name. Or we can use a nested ProviderScope and only log values inside a specific widget subtree.

This way we can evaluate state changes and monitor widget rebuilds without putting print() statements all over the place.

ProviderObserver is similar to the BlocObserver widget from the flutter_bloc package.

Conclusion

Riverpod borrows the best features of Provider and adds many benefits that make it easier and safer to manage state in our apps.

If you have an existing app that uses Provider, it is reasonably easy to migrate it to Riverpod.

For reference, I have ported my Starter Architecture App from Provider to Riverpod in less than one day. Check this updated tutorial for more information:

I have also built a Movie App that supports Provider, Riverpod, and flutter_bloc. You can browse the source code to learn about their differences.

For more information about Riverpod, check out the README on pub.dev and read the official documentation.

And if you have any feedback or comments about this guide, let me know on Twitter or Slack.

Happy coding!

Want more?

Fast-track your Flutter learning with over 40 hours of in-depth content.