Riverpod: A deep dive “on the surface”

Kefeh Collins
Flutter Community
Published in
13 min readOct 11, 2021

The Chronicles of Flutter state management 9.

The one controversy about flutter is the fact that there is way “too many” state management solutions and making the choice for which to use is a challenge. There is some truth to that as well as the fact that there is a mostly overlooked advantage to this, I will write a detailed article about this sometime soon, but the one advantage is that it is rather easier to have state management solutions that are almost just the perfect fit for your situation and not just a “one fix fits all” with redundancies and complexities that are not warranted.

So far, we have looked at state management solutions like stateful widgets, hooks, inherited widgets, Providers etc, which are for the most part a perfect fit for most small projects, and sometimes build on each other, upholding their benefits and trying to mitigate their drawbacks, such as Providers built to mitigate the drawbacks of Inherited widgets, but yet, due to design or certain constraints certain limitations could not be easily removed.

Riverpod: Its just a rearranged provider. Yeah literally, all characters from the word provider are used to spell riverpod.

So that’s it?

Nope: it’s more than just provider rearranged, it is also provider improved.

Providers as we know already was built upon inherited widgets adding their own perks to make it easier to use, intuitive, readable and all the good things it’s brought. But yet as growing codebases brought growing and increasing requirements, more of provider’s inadequacies were revealed.

Provider is highly dependent on the widget tree and build contexts, which in of itself is not a bad thing but it has its liability.

  • Combining providers is a hassle, bringing in more boilerplate [we hate boilerplate] and making code a little complicated to read. This is just because provider refuses when you want to pass a value that changes to another provider.

Let’s say for instance we have a credentials class that handles credentials and another class that handles authentication but depends on the credentials class, if we were to have both as providers, then combining them will look like this.

Too much boilerplate code for apparently no reason.

  • Runtime exceptions: This happens when we are providing a provider in a different widget subtree and then trying to access it in another, since providers search up in the search for the provider of that exact type, it can not find it and hence raises a not found exception. (This might seem easy to resolve, but in very huge codebases, it becomes difficult to track the widget branches and it’s therefore difficult to catch this always).
  • Providers customarily search up the widget tree to find the first instance of a provider. This works well because providers like inherited widgets rely on types but there is a problem; you can not or to put it better, it is difficult to have more than one provider of the same type, the one closest to the widget intended will always be accessed and hence causing unintended access.

Okay… enough about the inadequacies of providers, what then you may ask?

Riverpod:

It was built intentionally to be a better provider, from scratch it was built to escape the dependence on flutter, hence as a dart solution, it can be used with other dart UI frameworks like angular_dart and hence brings proper separation of UI from logic and improves on testing.

Consequently, since riverpod does not depend on flutter, it also does not depend on the widget tree and BuildContext, rather it has one widget which serves as the source of truth, holding references to all created riverpod providers. This widget when inserted into the widget tree, scopes all widgets under it to have access to the provider references. [We mostly place it as a parent to the MyApp widget so that it can be scoped to the entire app, but you might have other ideas, so place it wherever your creative mind tells you :)]

This widget is called ProviderScope (:) you see the word play there?)

Anyways before we look at ProviderScope, lets install riverpod

Riverpod comes in 3 different flavors

  1. Riverpod [for any dart project]
  2. Flutter_riverpod [for flutter specific projects]
  3. Hooks_riverpod [for those who love using hooks](checkout my hooks article if you want to)

We will be using flutter_riverpod for now so

To install it use

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

To use riverpod, we first 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.

Once a ProviderScope is inserted into our widget tree, we can create a simple provider with the Provider<T>() by creating a global variable.

This is just a simple way to inject dependencies to your app with riverpod.

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.

It goes without mentioning that Providers in riverpod are obtained by reference and not by type as with normal providers.

This makes them compile safe, the possibility of private providers and lastly, we can then have more than one provider of the same type since the providers can be declared as global (except you make them private or create them scoped to a widget)

But isn't global variables bad? As in everything can have access to the state?

No, as we mentioned earlier, providers though global are only obtained by reference, the state it returns can be scoped to just the widget accessing it (the state is not in the Provider but in the Scope that is, ProviderScope in this case).

So how do we use or access these providers then?

Inside a widget: we need access to the “ref” object (WidgetRef) this object allows widgets to interact with providers very akin to context and just like it, we have functions like read, watch and listen used to interact with providers.

In riverpod providers depend on WidgetRef and not BuildContext

So

context.read() becomes ref.read()

context.watch() becomes ref.watch()

context.listen() becomes ref.listen()

But WidgetRef is not directly accessible to widgets, riverpod provides multiple solutions to obtain one.

Using a Consumer() or HookConsumer() widget:

Here we can wrap our widget with a Consumer() widget that gives us access to a WidgetRef from which we can use to read or watch our provider.

This is also the same for HookConsumer() but the HookConsumer() widget comes from the hooks_riverpod package. it gives the same use case as Consumer but allows us to use hooks such as useState, useProvider etc

Using ConsumerWidget:

So instead of extending a StatelessWidget, we can just extend a ConsumerWidget, this makes the WidgetRef object accessible as a parameter to the build method, this solution is a little more elegant than using a Consumer() widget.

Lastly riverpod has ConsumerStatefulWidget and HookConsumerWidget:

These two are particularly important when you want to have a widget with its own internal state management and yet still be able to consume external or app level providers, for example, you might want to animate a widget and setting up the controllers for that animation will need some sort of state change.

We have so far seen how to inject a value, but using Provider<T>() we are unable to listen to changes, so in order to be able to listen to changes and rebuild when a change occurs, we have to use Providers that extends from a class AlwaysAliveProviderBase that contains the functions that permit change notification listening and rebuild.

For simple values that change over time we can use a simple.

StateProvider<T>()

Let’s say we have a square that displays the number of clicks in itself and when any instance of the square is clicked we display the total number of clicks

StateProvider<T>()

And then we consume it as

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 that may call for the need for an external class to hold the states with that we can use other solutions like ChangeNotifiers and StateNotifiers and each of them come with their respective Provider

With ChangeNotifiers

Change notifiers are very important, they give model classes the ability to emit a signal when a change occurs and hence can be listened to. If you want to know a little more about ChangeNotifiers, consider checking this article

Without further ado with change notifiers, we have the notifier class as (or model class)

And then the provider as

Then to watch and modify the clicks we use

With StateNotifier

StateNotifiers are a class ahead. It is built to uphold immutability, the one very important coding principle held so closely by programmers. With great justification, immutability is very vital and for states to be immutable is another very important as states are predictable and transparent. Read more here

So with StateNotifier we have

Then the provider as

And then we consume it as

We see some slight differences in that we use

ref.read(clicksChangeProvider.notifier).incrementClicks();

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.

when we need to read or watch just the state

ref.watch(clicksChangeProvider);

Read, Watch, listen, select

We have seen and used the ref.read(), ref.watch(), and will surely use ref.listen() but what are they?

  • ref.read()

As the name implies, we read a notifier and or its value and not trigger for changes. This is exactly the same thing we do when we use the Provider counterpart of context.read() or Provider.of(context, listen: false). This is very important as it gives us the possibility of accessing the provider and make changes outside of the build context that is, in the onTap() etc. Never call the ref.read() inside build context as much as it does not trigger rebuild of the widget when changes occur and could be seen as a way of preserving and reducing the number of useless UI rebuilds, it is highly contested against as the official docs for riverpod will say

Using ref.read this way is commonly associated with the thought “The value exposed by a provider never changes so using ‘ref.read’ is safe”.

The problem with this assumption is that, while today that provider may indeed never update its value, there is no guarantee that tomorrow will be the same.

Which will mean going through your code and changing stuff, not ideal practice.

  • ref.watch()

This is probably the most versatile of them all. It is used to get the value of a provider and also watch for changes. It is typically used only inside the build method or inside another provider. Never use it inside any of the lifecycle methods like initstate or setstate as there will be deadlock and conflict since both may be triggering a rebuild of the widget and still depending on each other

  • ref.listen()

This is a very useful one as it is similar in every way to ref.watch, but that it does not trigger a rebuild of the widget but instead it watches the provider and when a change occurs you can call a function. This is useful perhaps if for navigation etc.

  • provider.select()

The other day I was having a conversation with a friend who brought to my attention from logs that riverpod with ChangeNotifier rebuilds a few more times than provider and bloc and more rebuilds when used am immutable state like with statenotifier. This is understandable as few numbers of rebuilds is not a stronghold that riverpod is proud of, but the solution it provides is geared at different difficulties. But if you are concerned about the number of rebuilds you can mitigate this with the select method. This works in that, in a state or class with more than one property, you can decide to listen to changes only from one or a few properties (the provider listens to the whole object by default).

Before you would have something like.

Then to make it a little more picky, you can use

Other very useful providers in addition to changeNotifierProvider and StateNotifierProvider are the FutureProvider and StreamProvider which we use to handle asynchronous code like HTTP requests and then rebuild our UI when the data changes.

FutureProvider/StreamProvider:

This provider is somehow a combination of the Provider<T> and a FutureBuilder, 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.

As seen, tokenValue is a type of AsyncValue<T> 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 FutureProvider, or StreamProvider. Awesome right?

This is the same concept with StreamProviders, so we won’t look at an example.

Okay then, I may not have mentioned it but there are two types of “ref” objects.

  1. WidgetRef as we already know and have seen already
  2. ProviderReference we have seen it, though may not have taken note

This is the reference object that is passed to the callback when creating a provider. These two are very similar but are two distinct objects separated for good reason. The ProviderReference ref object gives us a reference to all providers and hence can have access to providers from within other providers then combining two or more providers can be hassle-free.

Lets see

And then combining them as

We use the ProviderReferece ref to access other references for providers and combining providers has never been this easy. Its very intuitive and straightforward.

Modifiers: family

It’s not just about combining providers. What if you want to be able to add external information to use to construct your provider that is not necessarily another provider. Let’s say we don’t save the tokens but have access to it in the widget already somehow, we may need to pass it as a parameter to the authenticate provider to revoke authentication. How do we do this?

As we see, we have added some “modifier” family to our FutureProvider that gives us a possibility to pass one more parameter in addition to the ProviderRef to our Provider. This is very useful in many scenarios.

This is fun and easy right?

But I have only shown how to pass one value, how about passing a few more parameters?

You can use

Alright, makes sense.

Modifiers: onDispose

There are situations in our app when we make a request and they are unexpectedly cancelled, so we may need to cancel the request or cache it, if we are using a provider, it means we need to dispose of the provider at that instance and handle some logic onDispose, but riverpod providers by default do not dispose of automatically, so we have another “modifier” called autoDispose. The autoDispose also gives us a boolean property called maintainState, which lets the provider know if after disposal the state should be maintained or not so that reinitialising it we could make some use of the state somehow.

To see this, let’s say we have an HTTP request that is triggered when a user enters a page and needs to be cancelled when they leave that page

Provider honestly in my opinion is very important and helps improve on certain principles of coding and make state management more intuitive and gives us more control of what we can or can not do.

Unfortunately, I did not cover tests in this article. But you can checkout the riverpod documentation for more information.

Thank you for reading.

References.

Follow Flutter Community on Twitter: @FlutterComm

--

--

Kefeh Collins
Flutter Community

An Enthusiastic problem solver, uses code as a tool for problem solving.