Chinedu Imoh Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Provider vs. Riverpod: Comparing state managers in Flutter

5 min read 1409

With so many options, it’s easy to get overwhelmed when choosing a state manager for your application. It seems that diverse state management solutions are released more often than one can imagine, each hoping to present a unique and easier way of handling the state.

In this article, we will be covering two different state management tools: Provider and Riverpod. We will briefly go over each tool to see the improvements Riverpod offers and why one might choose it over Provider, then highlight issues that Provider has with solutions that Riverpod provides.

This post assumes you are familiar with Flutter. Since it’s not an introduction to Riverpod or the Provider state management package, we won’t be going too deep into their features – only enough to point out the comparisons. This post focuses on Riverpod as a natural successor to Provider.

What is a state?

A state is a piece of information held by a widget when built, and can change when the widget refreshes. Certain data or information stored and passed across or within the widgets in an application is referred to as “the state.”

Everything in Flutter deals with handling and manipulating precise details, either retrieving them from, or displaying them to the user in one form or another. The method you choose to handle the state directly impacts the app’s behavior and security.

State management

State management refers to the techniques or methods used to handle the state in an application. State management techniques are numerous and fit various needs. There is no one-size-fits-all for any state management technique; you pick up the one that meets your needs and works best for you.

Riverpod

Riverpod is a state management package released by Remi Rousselet (the creator of Provider). Rousselet got the word Riverpod by rearranging the letters of the word “Provider.”

Riverpod was built primarily to solve Provider’s flaws (we will discuss a few of those flaws later on). It is fast and easy to use and comes right out of the box as a quick, lightweight package for state management.

Since its official release, Riverpod has been creating waves across the state management community because of its straightforward, yet very powerful, handling of the state in a compile-safe manner.

In Riverpod, you declare the provider and call it anywhere you’d like to make use of it. Riverpod is easy, simple, and fast.

We made a custom demo for .
No really. Click here to check it out.

Check out this example of state management with Riverpod. First, we wrap our entire app inside a ProviderScope. ProviderScope scopes all providers created in the app and makes it possible to use any declared provider globally:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() => runApp(ProviderScope(child: RiverPodApp()));

Next, declare the provider:

final favoriteProvider = ChangeNotifierProvider((ref) => new Favorites());

Here we are making use of ChangeNotifierProvider. This provider will always provide us with the Favorites class.

To make use of the providers inside our widget, extend the ConsumerWidget:

class RiverPodApp extends ConsumerWidget {
 const RiverPodApp({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context, ScopedReader watch) {
  return MaterialApp(
   home: Scaffold(
    appBar: AppBar(
     title: Text('My favorite fruit is ' + watch(favoriteProvider).fruit),
    ),
    body: Center(
     child: Column(
      children: [
       FruitButton('Apples'),
       FruitButton('Oranges'),
       FruitButton('Bananas'),
      ],
     ),
    ),
   ),
  );
 }
}

Note that the ConsumerWidget gives us access to the ScopedReader inside the build method, which provides access to the contents of the provider.

In this example, we have created three buttons for different fruits. When each button is clicked, the name of the fruit changes in the app bar. When you start up the app, the app bar reads, “My favorite fruit is unknown.” When each fruit button is clicked, the name of the fruit changes.

The change is possible because the app bar watches the variable fruit created in the Favorite class (by default, it’s called “unknown”). When each button is clicked, the changeFruit function is called, assigning the fruit variable a new value and updating the widget:

class FruitButton extends StatelessWidget {
 final String fruit;
 FruitButton(this.fruit);
 @override
 Widget build(BuildContext context) {
  return ElevatedButton(
   child: Text(fruit),
   onPressed: () {
    context.read(favoriteProvider).changeFruit(fruit);
   },
  );
 }
}
class Favorites extends ChangeNotifier {
 String fruit = 'unknown';
 void changeFruit(String newFruit) {
  fruit = newFruit;
  notifyListeners();
 }
}

Why choose Riverpod?

Listed below are various reasons why one might choose Riverpod:

  • Riverpod is compile-time safe
  • It doesn’t depend directly on the Flutter SDK
  • Riverpod can be used to create and enforce a one-direction data flow with model classes that are immutable (meaning they do not change)
  • Riverpod does not directly depend on the widget tree; its operation is similar to a service locator. The providers are declared globally and can be used anywhere in the application
  • Riverpod gives widgets access to the providers through ScopedReader, which is passed to the build method, and finally consumed via the ConsumerWidget class

Issues with Provider that Riverpod solves

There are several flaws with Provider that Riverpod solves.

First, unlike Riverpod, Provider is dependent solely on Flutter. Because its widgets are used to provide objects or states down the tree, it exclusively depends on Flutter, resulting in a blend of UI code and dependency injections.

On the other hand, Riverpod is not reliant on widgets; you can declare a provider in Riverpod and use it anywhere in the application, regardless of the parent widget. Providers in Riverpod are declared as global variables and placed inside any file.

Provider also relies only on the object type to resolve the object requested by the widget. If you provide two of the same kind, you can only get one closer to the call site. However, Riverpod supports multiple providers of the same type, which you can use anywhere and anytime.

With Provider, if you try to access a non-provided type, you will end up with an error at runtime. This runtime error should not be so, because we should catch as many errors as possible while compiling the app. Riverpod solves this by catching errors during the compilation of the app, making the user experience more seamless.

Combining two or more providers can lead to terribly nested code. Riverpod handles this problem using ProviderReference. Providers’ dependencies are injected and called anytime, meaning that a provider can depend on another provider and be called easily through ProviderReference.

Here’s an example:

Future<void> main() async {
 WidgetsFlutterBinding.ensureInitialized();
 final sharedPreferences = await SharedPreferences.getInstance();
 runApp(MultiProvider(
  providers: [
   Provider<SharedPreferences>(create: (_) => sharedPreferences),
   ChangeNotifierProxyProvider<SharedPreferences, HomeViewModel>(
    create: (_) => HomeViewModel(sharedPreferences),
    update: (context, sharedPreferences, _) =>
      HomeViewModel(sharedPreferences),
   ),
  ],
  child: Consumer<HomeViewModel>(
   builder: (_, viewModel) => HomeView(viewModel),
  ),
 ));
}

In this example, we have HomeView, which takes a HomeViewModel argument. But because HomeViewModel depends on SharedPreferences, we need the MultiProvider and ProxyProvider widgets to put everything together.

With that in mind, we can see that there’s too much boilerplate code. It would be better if all of these providers were outside the widget instead of inside the widget tree.

In comparison, here’s an example of a provider depending on another provider in Riverpod with none of the nesting problems Provider presents:

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

final authenticateFBUser = FutureProvider<void>(
  (ref) async {
    final authFBResult = await ref.read(authProvider).login();
    ref.read(appTokenProvider).state = authFBResult.token;
  },
);

In the example above, the authenticateFBUser provider depends on the appTokenProvider, which it calls through the ProviderReference (ref) Riverpod provides.

Comparing Provider and Riverpod

Here are a few comparisons between Provider and Riverpod:

  • Runtime exceptions exist with Provider, but they are handled and corrected with Riverpod
  • Provider is not compile-safe while Riverpod is
  • In Provider you can’t declare multiple providers of the same type, while in Riverpod, you can do this without overriding the others
  • You can declare the provider and its class without scattering the app’s root file in Riverpod
  • In Riverpod, the providers are declared globally and can be used anywhere in the app using either the Consumer widget or context.read
  • In Provider, dependency can lead to horribly nested code, while it’s easy in Riverpod for a provider to consume another using ProviderReference

Conclusion

As I mentioned earlier, Riverpod is Provider’s successor, and they were both created by Remi Rousselet. Riverpod can be seen as Provider without the shortcomings; it corrected many flaws Provider has.

However, as stated earlier, every state management package has its highs and lows, and it all depends on your specific use case. I hope this post provided you with the necessary comparisons to make a proper decision between the two options.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Chinedu Imoh Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Leave a Reply