DEV Community

Cover image for Riverpod: Rewriting Provider
Marcos Sevilla
Marcos Sevilla

Posted on • Updated on

Riverpod: Rewriting Provider

I think it's not an exaggeration to say that most of the content on Flutter is about state management. Being a reactive framework for the client side, Flutter has integrated the concept of state since its birth. But if you do a complex project, setState won't solve your problems, that's why there are state managers.

Flutter's most popular state management tool is Provider. I don't say it, science says it:

Provider stats in pub.dev

Followed by packages like Bloc, which even depends on the same Provider, that's how useful it is.

I'm not going to talk much about Provider because I already made a clarification that I consider important in my article on StateNotifier, it's a must read since it clarifies quite what Provider is (I'm sure you already know what it is) and it is one of the packages that Riverpod incorporates.

But... what is Riverpod and why do I like it so much?

Cause

In the StateNotifier article I explained that Provider was a tool that allows us to easily use InheritedWidget, which is the class in Flutter that gives access to its properties to its children in the widget tree.

Provider itself has a lot of problems because of this relationship with InheritedWidget.

First, doesn't allow us to create two Providers of the same type. If we have a class from which we want to create two separate objects to handle similar logic in different modules of the app, it's impossible for us because an InheritedWidget is found in the tree looking for a specific data type -or class-. This also causes Providers not to be found and leaves us runtime errors that could be avoided in compilation. Strike one.

Then combining Providers can be cumbersome, more complex than it should, and throw unexpected errors from the same unusual InheritedWidget manipulation. The common way to make a Provider aware of changes in another to react in its logic is by using a ProxyProvider orChangeNotifierProxyProvider and yet these aren't the best alternatives in synchronization or syntax (it gets very nested) , they are a dependency injection and not exactly a listener for events. Strike two.

Finally, depending on InheritedWidget, it depends on Flutter, which isn't the best if you like to encapsulate your business logic in a separate package and maintain a separation of concerns with your logic independent of the SDK of the user interface. Strike three ... well, not necessarily, but do you see that there are many opportunities for improvement?

Effect

All these problems make the creator of Provider -I stand up and applaud- Rémi Rousselet, had the initiative to reengineer InheritedWidget from scratch, thus creating Riverpod.

Riverpod is currently in a stable version for production use, but as such it's an experimental project that isn't an official replacement for Provider, yet.

In this link you can see the status of Riverpod according to Rémi.

In simple words, Riverpod doesn't depend on Flutter so it doesn't depend on InheritedWidget, it allows us to combine Providers to listen to and react to its changes and it guarantees us reliable code from the compilation stage.

Yes, it solves Provider's problems, and very well if you ask me.

Let's see it in action and its equivalents with respect to Provider.

Which package do I use

https://awkwardvibes.com/wp-content/uploads/2020/03/spiderman-pointing-gif.gif

If you do a search in pub.dev, you will find 3 versions of Riverpod, let me explain what each one does.

  • riverpod - Contains Riverpod's core, the basic functionality to use it without depending on Flutter. This is the one to use if you are creating a package for a business logic layer other than the graphical interface.
  • flutter_riverpod - Contains Riverpod's core, plus all the classes and widgets used to link the logic with the interface, depends on Flutter. It's the one that you must use if your project already needs Flutter and you are not separating the logic in another package outside the project. The most common because of how you organize your projects.
  • hooks_riverpod - Contains Riverpod's core and additionally the necessary functionality to be used in conjunction with flutter_hooks, which must also be added as a dependency. It's the one you should use if your project uses hooks to simplify your code in the graphical interface and Riverpod to access the state. I'm not going to go too deep into this package in this article.

And if it wasn't clear, this image is in the documentation, to make it more graphic:

Which Riverpod do I use

Migrating from Provider to Riverpod

From MultiProvider to ProviderScope

Initially when we wanted to insert a Provider in the widget tree, we did something like this ...

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider(create: (_) => MyProvider()),
      ],
      child: MyApp(),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

Here we could have a list of our Providers and it depended on the MultiProvider, a single Provider or ChangeNotifierProvider. This way gave us a scopethat determined where in our widget tree we would be able to use our logic holder.

In Riverpod, our Providers can be seen as global variables that we can also consume in a global scope called ProviderScope and this scope can be overwritten as necessary in the tree.

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

New way to create Providers

Once our ProviderScope is declared, we can use any Provider below it. Now let's see how we make a Provider.

In the case of Provider, we create a class that inherits from ChangeNotifier, StateNotifier or a repository or service class of its own. Riverpod comes with StateNotifier by default, so it's better to implement this alternative, same way there are more types of Providers than we will talk about in a moment.

class NamesNotifier extends StateNotifier<List<String>> {
  NamesNotifier([List<String>? initialNames]) : super(initialNames ?? []);

  void add(String newName) {
    state = [...state, newName];
  }
}
Enter fullscreen mode Exit fullscreen mode

We can use this NamesNotifier both in Provider and Riverpod because the logic components are not affected by the package, which already takes care of incorporating this component into the widget tree for use.

In Provider we need to include the packages state_notifier and flutter_state_notifier, in Riverpod does not need them since flutter_riverpod contains the ConsumerWidget (equivalent to the Consumer widget in Provider but used more similar to StatelessWidget).

💡 I quickly clarify that the core of the packages is in riverpod and state_notifier, and the other packages that I mentioned that are called the same with the difference of the flutter prefix contain the widgets to communicate the logic of these packages with the interface.

To create a StateNotifierProvider on Riverpod, use the following way...

final myProvider = Provider((ref) => MyProvider());
Enter fullscreen mode Exit fullscreen mode

And to create a StateNotifierProvider in Riverpod it's:

final namesNotifierProvider = StateNotifierProvider((_) => NamesNotifier());
Enter fullscreen mode Exit fullscreen mode

Once declared, no matter what file it's in, the Provider is in the ProviderScope and is available wherever it is needed via ProviderReference.

I want you to analyze the syntax a bit.

  1. We declare them as a final variable.
  2. The Provider is created with the class Provider, which contains a callback function by which we return the class. Note: this function will serve us much later.
  3. The callback returns a variable of type ProviderReference, which we call ref, for our use in creating the Provider.

The ref variable is what would be a BuildContext for Providers in Riverpod. With it we can access other Providers in the tree and the good thing is that we can access it in the creation of any Provider.

See why it was good to separate him from Flutter? We no longer need BuildContext.

Use of Providers in the graphical interface

In Provider we use the BuildContext of the build() method of any widget to access our Provider since it is anInheritedWidget.

We can do it in two ways...

  • Use ConsumerWidget.
class WidgetToShowNames extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final names = watch(namesNotifier.state);

    return ListView.builder(
      itemCount: names.length,
      itemBuilder: (_, index) {
        final name = names[index];
        return Text(name);
      }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Use Consumer widget.
class WidgetToShowNames extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (_, watch, __) {
        final List<String> names = watch(namesNotifier).state;

        return ListView.builder(
          itemCount: names.length,
          itemBuilder: (_, index) {
            final name = names[index];
            return Text(name);
          }
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Yes, there's a ConsumerWidget that is very similar to aStatelessWidget, but adds a ScopedReader as a parameter to the build() method. This new parameter allows us to access any Provider that we create and be listening (or observing) the changes it may have.

Likewise, we can use a StatelessWidget in conjunction with the Consumer widget that works in the same way as in Provider, only that it gives us our ScopedReader instead of the type that we define, since there can be multiple Providers with the same type and that no longer guarantees that we will get the value we want when sending the type.

💡 In this section of What Marcos Recommends...

I use ConsumerWidget since I don't like to nest more widgets with Consumer, the code seems much more readable to me and it is easier to find where your Providers are used having classified your widgets in StatelessWidget, StatefulWidget and ConsumerWidget.

Combining Providers

One of the solutions that Riverpod brings to Provider problems is easier communication between Providers. We achieve this thanks to the ProviderReference.

The creation of a Provider gives us by default its possible combination with another that is in the reference. This is how we can inject dependencies of Providers and establish behaviors based on those changes.

final tokenProvider = StateProvider<String>((_) => '');

final authenticateUser = FutureProvider<void>(
  (ref) async {
    final authResult = await ref.read(authProvider).login();
    ref.read(tokenProvider).state = authResult.token;
  },
);
Enter fullscreen mode Exit fullscreen mode

Suppose we have a Provider with authentication functionality, which we call authProvider, we use its login() method that returns an access token. Having the token, we can update the state of our tokenProvider, which saves the value for us to use in other queries.

What? What's that StateProvider and FutureProvider thing? Glad you ask...

Everything is a Provider

As you can see, there are many types of Providers, and it's a word that's going to come up everywhere if you work with Riverpod.

Just like Flutter tells you to consider everything a widget in the framework, at Riverpod I suggest you consider everything a Provider. A Provider can be a function (they are built with functions themselves), a state variable, a global variable (literally), and so on.

Provider types

I'm going to present you the most common and used types of Providers so far.

  • StateProvider - Exposes a value that can be modified from outside. As we saw in the example before, we access the state through the state variable and we can change it. Very similar to ValueNotifier.
  • FutureProvider - Constructs an asynchronous value or AsyncValue and has the operation of an ordinaryFuture. It's useful to load data from a service whose method to obtain this data is asynchronous, as we saw in the example with a value that comes from an asynchronous call and then saves it in a StateProvider.
  • StreamProvider - Create and expose the last value in a Stream. Ideal for communication in real time with Firebase or some other API whose events can be interpreted in Stream.
  • StateNotifierProvider - Create a StateNotifier and expose its state. The most used for logic components, it allows easy access to state, which is usually a private property that's modified by public methods.
  • ScopedProvider - Defines a Provider<T> (of any type) that behaves differently somewhere in the project. It's used in conjunction with the overrides property of ProviderScope and defines with the latter the scope where this ScopedProvider is going to be modified.
  • ProviderFamily - This type of Provider is a modifier, that is, all of the above can use it. In itself, it defines an additional parameter that's involved in the creation of our Provider, such as a String.
final usersFamily = FutureProvider.family<User, String>(
    (ref, id) async => dio.get('https://my_api.dev/users/$id');
);
Enter fullscreen mode Exit fullscreen mode
  • AutoDisposeProvider - Another modifier that allows us to automatically dispose() our Providers, allowing us to delete the state when the widgets where they are used are destroyed.
final numbersProvider = StreamProvider.autoDispose(
    (ref) {
      final streamController = StreamController<int>();

      ref.onDispose(() {
        streamController.close();
      });

      return streamController.stream;
    },
);
Enter fullscreen mode Exit fullscreen mode

https://media.giphy.com/media/kSlJtVrqxDYKk/giphy.gif

Best practices

How to structure Providers

Riverpod has the peculiarity that, as we saw previously, it covers many use cases with different types of Providers, from app state to ephemeral state.

The problem is that there is no guide on how to structure Providers, so I always recommend separating your logic in a good way.

I summarize it in my personal rules:

  • Separate project folders by functionalities (features) and core.
  • In the core, use a /providers folder and separate each Provider that you are going to use in multiple parts of your app in a file. For example, I always use package_info with a Provider in the core of my projects.
  • In a specific functionality I try to have a /logic folder where I have all my Providers.
  • I don't have more than one logic holder (StateNotifier) for functionality.
  • In logic I separate my state, logic holder and Provider in separate files.

With the rules that I mentioned before, we have a folder structure like this...

Providers structure

Some things to consider

I was thinking for a long time if there are any downsides or something to say as a warning about Riverpod. Being honest and objective, I don't find many things and the ones I do find aren't critical.

Riverpod is already in a stable phase, as you can see. You can implement it in your projects from now on and I dare to say that it may be a better option than Provider. It's a package that can take your time to master, but once you do, it becomes a very capable tool.

I also clarify that it isn't a replacement for Provider yet, since Provider still solves many needs. For me, Riverpod is going to be the new standard once more developers master it and the problems it solves are understood, as many have yet to find scenarios where Provider shows its flaws.

https://media.giphy.com/media/ZwvpEzDz05oc0/giphy.gif

As always...

You can share this article to help another developer to continue improving their productivity when writing applications with Flutter.

There's a Spanish version of this article on Medium. You're welcome. 🇪🇸

Also if you liked this content, you can find even more and keep in contact with me on my socials:

  • dev.to - where you're reading this article.
  • GitHub - where are my code repositories in case you like the examples.
  • LinkedIn - where I connect professionally.
  • Medium - where I publish my Spanish articles.
  • Twitter - where I express my short thoughts and share my content.
  • Twitch - where I do informal live shows from which I take clips with specific information.
  • YouTube - where I publish the clips that come out of my lives.

Discussion (4)

Collapse
camillo777 profile image
camillo777

Really good article with practical suggestions and examples and information that you cannot find anywhere on the internet. Usually RiverPod tutorials repeat exactly the RiverPod docs. This is different, explains how to organize well the project files.

Collapse
camillo777 profile image
camillo777

BTW should be updated to RiverPod 0.14 and I think to next version 1.0 when it will be available soon!

Collapse
marcossevilla profile image
Marcos Sevilla Author

yes! I'll wait for the breaking changes to further update the article :)

Collapse
maiconcrespo profile image
Maicon Crespo

Great Article, I study riverpod with all your article and now I'm learnign bloc,

Hey now for bloc, have a river_bloc package ....check in pubDev!