Riverpod: Rewriting Provider

Riverpod: Rewriting Provider

Understanding Riverpod and not die trying

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 package:provider. Ok, maybe there's another one I'm not recommending nor mentioning, but provider it's probably the one that most state management packages use or take inspiration from. At the time I'm writing this, 396 packages depend on provider according to pub.dev.

I'm not going to talk much about provider because I already made a clarification that I consider important in my article on state_notifier. That article is a must-read since it clarifies quite what provider 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 state_notifier 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 some problems because of this relationship with InheritedWidget.

First, doesn't allow us to create two Providers of the same type unless they are on a different subtree. 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 the 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 or ChangeNotifierProxyProvider and yet these aren't the best alternatives in synchronization or syntax (it gets very nested). You can make hacky workarounds for this but I'm not into that.

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 made the creator of provider, Rémi Rousselet, have the initiative to re-engineer InheritedWidget from scratch, thus creating package:riverpod.

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

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

  • package:riverpod: Contains the core 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.

  • package:flutter_riverpod: Contains the core, plus all the classes and widgets used to link the logic with the user interface, depending 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. It's the most common because of how projects are usually organized.

  • package:hooks_riverpod: Contains the core and adds the necessary functionality to be used in conjunction with package: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 user 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 clear:

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(),
    ),
  );
}

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 scope that determined wherein our widget tree we would be able to use our logic component.

In riverpod, our Providers can be used 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(),
    ),
  );
}

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, a repository or service class of its own. riverpod comes with StateNotifier by default, so it's easier 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];
  }
}

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, these are not needed 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 user interface.

To create a Provider on riverpod, use the following:

final myProvider = Provider((ref) => MyProvider());

And to create a StateNotifierProvider in riverpod we do this:

final namesNotifierProvider = StateNotifierProvider<NamesNotifier, List<String>>((_) => NamesNotifier());

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

I want you to analyze the syntax a bit.

  1. We declare them as a final variable.

  2. We instance 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 ProviderRef, 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 with Widgets

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 {
  const WidgetToShowNames({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final names = ref.watch(namesNotifierProvider);
    return ListView.builder(
      shrinkWrap: true,
      itemCount: names.length,
      itemBuilder: (_, index) {
        final name = names[index];
        return Text(name);
      },
    );
  }
}
  • Use Consumer widget.
class WidgetToShowNames extends StatelessWidget {
  const WidgetToShowNames({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        final names = ref.watch(namesNotifierProvider);
        return ListView.builder(
          shrinkWrap: true,
          itemCount: names.length,
          itemBuilder: (_, index) {
            final name = names[index];
            return Text(name);
          },
        );
      },
    );
  }
}

Yes, there's a ConsumerWidget that is very similar to aStatelessWidget, but adds a WidgetRef 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 WidgetRef 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 ProviderRef.

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 authenticateUser = FutureProvider<String>(
  (ref) async {
    await ref.read(authProvider.notifier).login();
    return ref.read(authProvider);
  },
);

final authProvider = StateNotifierProvider<AuthNotifier, String>(
  (_) => AuthNotifier(),
);

class AuthNotifier extends StateNotifier<String> {
  AuthNotifier() : super('no-token');

  Future<String> login() async {
    return await Future.delayed(
      const Duration(seconds: 3),
      () => state = 'token',
    );
  }
}

Suppose we have a Provider with authentication functionality, which we call authProvider, we use its login method that sets an access token as its state. Having the token in the state, we can use that state to call an authentication function and save the value for other queries.

What? What's that 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 to you the most common and used types of Providers so far.

  • StateProvider: Exposes a value that can be modified from outside. 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 ordinary Future. It's useful to load data from a service whose method to obtain this data is asynchronous.

  • 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 a generic 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'),
);
  • AutoDisposeProvider: Another modifier that allows us to automatically dispose of 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;
    },
);

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).

  • In a specific functionality I try to have a /provider folder where I have all my Providers.

  • I don't have more than one logic component (StateNotifier) for functionality.

  • In the provider folder, I separate my state, logic component, and Provider in separate files. You can also have a generated freezed file if you use that tool for state union classes.

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

carbon.png

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 becoming a real state management option since more developers are mastering it and the problems it solves are valued. Many have yet to find scenarios where provider shows its flaws.

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