Flutter Riverpod: The Essential Guide

António Nicolau
7 min readSep 3, 2022

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.

That a powerful state management library that allow you:

  • 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

In this article I’m going to guide through this amazing tool and help you understand it once and for all.

You need to be aware that Riverpod is spread across multiple packages, with slightly different usage.

The variant of Riverpod that you will want to install depends on the app you are making

Multiple packages to work with Riverpod

To understand better which package to use looks at this illustration

Credit: blog.csdn

In this article we’ll talk about flutter_riverpod and cover other types in the next articles.

Getting started

First of all you need to add flutter_riverpod as dependence in your pubspec.yaml , you can do it by running flutter pub add flutter_riverpod or open your pubspec.yaml file and add

dependencies:
flutter_riverpod: ^1.0.4

Now you can import it like this

import 'package:flutter_riverpod/flutter_riverpod.dart';

Now that you have it installed let’s see how you can work with it

First, we need to wrap our main widget with the ProviderScope widget (or any widget in the tree, for a different scope). This widget is where all the states of providers will be stored.

Wrap main widget with ProviderScope

And once you wrapped your main widget with ProviderScope you can create a simple provider like this

final helloWorldProvider = Provider<String>((ref)=> "hello world");

It create a simple provider that exposes as its state a “hello world” string

The following example shows how to use riverpod to make a simple “Hello world”

Simple “hello world” with riverpod
  1. Created a simple provider that return “hello world” final helloWorldProvider = Provider<string>((ref)=> “hello world”);
  2. Read provider value using ref.watch/ref.read final String value = ref.watch(helloWorldProvider);
  3. Show its value in a Text widget Text('Provider says: $value)

That’s it, super simple right ?

Provider<T>, exposes a read-only value. It’s the most simple kind of provider, commonly used as a “cache” to share data (model classes) among widgets.

Different Types of Providers

We already talked about one provider type Provider<T> the most simplest one but there are multiple types of providers for multiple different use cases.

With all of these providers available, it is sometimes difficult to understand when to use one provider type over another. Use the table below to choose a provider that fits what you want to provide to the widget tree.

Different types of providers

NOTE FROM DOCUMENTATION: While all providers have their purpose, ChangeNotifierProviders are not recommended for scalable applications, because of issues with mutable state. It exists in the flutter_riverpod package to provide an easy migration path from package:provider, and allows for some flutter specific use-cases such as integration with some Navigator 2 packages.

Lets take a look at those different type of providers to learn how you can use it in your project

StateProvider

StateProvider is a provider that exposes a way to modify its state. It is a simplification of StateNotifierProvider, designed to avoid having to write a StateNotifier class for very simple use-cases.

StateProvider exists primarily to allow the modification of simple variables by the User Interface

It’s really easy to work with, you can create one this way

Note that since it is a StateProvider, the state is made available using the state variable and can be updated as seen in the onTap function call.

But states are never one-off types as seen, rather there is more to it in bigger projects, depending on the needs you might use a class to hold the states so the sky is the limit 😉

StateNotifierProvider

StateNotifierProvider is a provider that is used to listen to and expose a StateNotifier.
This along with StateNotifier is Riverpod's recommended solution for managing state which may change in reaction to a user interaction.

We see some slight differences in that we use

When we want to read or watch a StateNotifierProviders notifier as it exposes the notifier and not its value, this helps as just seen to give us access to the methods and notifier properties.

ref.read(clickCountProvider.notifier).increment();

When we need to read or watch just the state without access the notifier

ref.watch(clickCountProvider);

FutureProvider/StreamProvider:

FutureProvider is the equivalent of Provider but for asynchronous code, in this sense you can create a synchronous value and watch for changes to it, like it being successfully retrieved.

Riverpod removes the redundancies of using something like FutureBuilder and automatically handles the code.

For the example above we used https://catfact.ninja/fact api, a free api that get random cat facts via text message every day.

As seen, catfact is a type of AsyncValue<String> which provides the when method with options for data, loading, and error that we can then manipulate as required. This is much easier than using a FutureBuilder or StreamBuilder.

This is the same concept with StreamProviders, the only thing that really change is the provider name 😅 from FutureProvider to StreamProvider.

Reading a Provider: read, watch, listen, select

We’ve been using ref.read(), ref.watch but what are they ? lets look at those and also take a look at ref.listen() and select

ref.read

In simple worlds the ref.read method is a way to obtain the state of a provider without listening to it.

About ref.read there’s some important notes

Using ref.read should be avoided as much as possible because it is not reactive.

It exists for cases where using watch or listen would cause issues. If you can, it is almost always better to use watch/listen, especially watch.

ref.watch

ref.watch is used inside the build method of a widget or inside the body of a provider to have the widget/provider listen to a provider. It means that when you want to listen for provider states changes the ref.watch is the right choice, use it instead of ref.read.

final counterProvider = StateProvider((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

The watch method should not be called asynchronously, like inside an onPressed of an ElevatedButton. Nor should it be used inside initState and other State life-cycles.

In those cases, consider using ref.read instead.

ref.listen

That’s similarly to ref.watch, it is possible to use ref.listen to observe a provider.

The main difference between them is that, rather than rebuilding the widget/provider if the listened to provider changes, using ref.listen will instead call a custom function.

For example, if we have a authErrorCodeProvider that store the returned auth request status and we want to show modal with error message or redirect to home page based on this error code, we could listen to this provider changes and call a function based on returned value.

ref.listen example

Filter rebuilds using “select”

One final feature to mention related to reading providers is the ability to reduce the number of times a widget/provider rebuilds from ref.watch, or how often ref.listen executes a function.

By default, listening to a provider listens to the entire object state. But sometimes, a widget/provider may only care about changes to some properties instead of the whole object.

For example, a provider may expose a User object:

class User {
String get name;
int get age;
}

But a widget may only use the user name:

Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

If we naively used ref.watch, this would rebuild the widget when the user's age changes but we just need to listen for name property changes

The solution is to use select to explicitly tell Riverpod that we only want to listen to the name property of the User.

The updated code would be:

Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}

Whenever the User changes, Riverpod will call this function and compare the previous and new result. If they are different, Riverpod will rebuild the widget.

However, if they are equal, Riverpod will not rebuild the widget. That’s really nice when we want to avoid unnecessary widget/provider rebuilds, give it a try 😉

Provider Modifiers

At the moment, there are two modifiers available:

  • autoDispose, which will make the provider automatically destroy its state when it is no longer being listened to.
  • .family, which allows creating a provider from external parameters.
final myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0);
final myFamilyProvider = Provider.family<String, int>((ref, id) => id);

A provider can use multiple modifiers at once:

final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
return fetchUser(userId);
});

Riverpod is a powerful state management and when you learn to work with it, becomes an important ally. It helps improve on certain principles of coding and gives us more control of what we can or can not do.

I did not cover tests in this article. But I’m already writing a new article covering this topic so follow to get informed when.

Thank you for reading 😊 give a clap if you liked👏 and follow for more Flutter tips

--

--

António Nicolau

Make learning Android, iOS, and software development more fun and easier 💻🖱️