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.
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 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 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.
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(); } }
Listed below are various reasons why one might choose Riverpod:
ScopedReader
, which is passed to the build method, and finally consumed via the ConsumerWidget
classThere 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.
Here are a few comparisons between Provider and Riverpod:
Consumer
widget or context.read
ProviderReference
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.