Riverpod for Provider users
This article is designed for people familiar with the Provider package who wants to learn about Riverpod.
The relationship between Riverpod and Provider
Riverpod is designed to be the spiritual successor of Provider. Hence the name "Riverpod", which is an anagram of "Provider".
Riverpod was born while searching for solutions to the various technical limitations that Provider face. Originally, Riverpod was supposed to be a major version of Provider as a way to solve this problem. But it was decided against as this would be a decently big breaking change, and Provider is one of the most used Flutter packages.
Still, conceptually, Riverpod and Provider are fairly similar.
Both packages fill a similar role. Both try to:
- cache and dispose some stateful objects
- offer a way to mock those objects during tests
- offer a way for Widgets to listen to those objects in a simple way.
At the same time, think of Riverpod as what Provider could've been if it continued to mature for a few years.
Riverpod fixes various fundamental problems with Provider, such as but not limited to:
- Significantly simplifying the combination of "providers".
Instead of the tedious and error-prone
ProxyProvider
, Riverpod exposes simple yet powerful utilites such as ref.watch and ref.listen. - Allowing multiple "provider" to expose a value of the same type.
This removes the need for defining custom classes when exposing a plainint
orString
would work just as well. - Removing the need to re-define providers inside tests. With Riverpod, providers are ready to use inside tests by default.
- Reducing the over-reliance on "scoping" to dispose objects by offering alternate ways to dispose objects (autoDispose) While powerful, scoping a provider is fairly advanced and hard to get right.
... And a lot more.
The only true downside of Riverpod is that it requires changing the widget type to work:
- Instead of extending
StatelessWidget
, with Riverpod you should extendConsumerWidget
. - Instead of extending
StatefulWidget
, with Riverpod you should extendConsumerStatefulWidget
.
But this inconvenience is fairly minor in the grand scheme of things. And this requirement might one day disappear.
So to answer the question you're probably asking yourself:
Should I use Provider or Riverpod?
You probably should be using Riverpod.
Riverpod is overhaul better designed and could lead to drastic simplifications
of your logic.
The difference between Provider and Riverpod
Defining providers
The primary difference between both packages is how "providers" are defined.
With Provider, providers are widgets and as such placed inside the widget tree,
typically inside a MultiProvider
:
class Counter extends ChangeNotifier {
...
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(create: (context) => Counter()),
],
child: MyApp(),
)
);
}
With Riverpod, providers are not widgets. Instead they are plain Dart objects.
Similarly, providers are defined outside of the widget tree, and instead are declared
as global final variables.
Also, for Riverpod to work, it is necessary to add a ProviderScope
widget above the
entire application. As such, the equivalent of the Provider example using Riverpod
would be:
// Providers are now top-level variables
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());
void main() {
runApp(
// This widget enables Riverpod for the entire project
ProviderScope(
child: MyApp(),
),
);
}
Notice how the provider definition simply moved up a few lines.
Since with Riverpod providers are plain Dart objects, it is possible to use
Riverpod without Flutter.
For example, Riverpod can be used to write command line applications.
Reading providers: BuildContext
With Provider, one way of reading providers is to use a Widget's BuildContext
.
For example, if a provider was defined as:
Provider<Model>(...);
then reading it using Provider is done with:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
Model model = context.watch<Model>();
}
}
The equivalent in Riverpod would be:
final modelProvider = Provider<Model>(...);
class Example extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
Model model = ref.watch(modelProvider);
}
}
Notice how:
Riverpod's snippet extends
ConsumerWidget
instead ofStatelessWidget
. That different widget type adds one extra parameter to ourbuild
function:WidgetRef
.Instead of
BuildContext.watch
, in Riverpod we doWidgetRef.watch
, using theWidgetRef
which we obtained fromConsumerWidget
.Riverpod does not rely on generic types. Instead it relies on the variable created using provider definition.
Notice too how similar the wording is. Both Provider and Riverpod use the keyword "watch" to describe "this widget should rebuild when the value changes".
Riverpod uses the same terminology as Provider for reading providers.
BuildContext.watch
->WidgetRef.watch
BuildContext.read
->WidgetRef.read
The rules for context.watch
vs context.read
applies to Riverpod too:
Inside the build
method, use "watch". Inside click handlers and other events,
use "read".
Reading providers: Consumer
Provider optionally comes with a widget named Consumer
(and variants such as Consumer2
)
for reading providers.
Consumer
is helpful as a performance optimization, by allowing more granular rebuilds
of the widget tree – updating only the revelant widgets when the state changes:
As such, if a provider was defined as:
Provider<Model>(...);
Provider allows reading that provider using Consumer
with:
Consumer<Model>(
builder: (BuildContext context, Model model, Widget? child) {
}
)
Riverpod has the same principle. Riverpod, too, has a widget named Consumer
for the exact same purpose.
If we defined a provider as:
final modelProvider = Provider<Model>(...);
Then using Consumer
we could do:
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
Model model = ref.watch(modelProvider);
}
)
Notice how Consumer
gives us a WidgetRef
object. This is the same object
as we saw in the previous part related to ConsumerWidget
.
Combining providers: ProxyProvider with stateless objects
When using Provider, the official way of combining providers is using the
ProxyProvider
widget (or variants such as ProxyProvider2
).
For example we may define:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
From there we have two options. We may combine UserIdNotifier
to create a new
"stateless" provider (typically an immutable value that possibly override ==).
Such as:
ProxyProvider<UserIdNotifier, String>(
update: (context, userIdNotifier, _) {
return 'The user ID of the the user is ${userIdNotifier.userId}';
}
)
This provider would automatically return a new String
whenever
UserIdNotifier.userId
changes.
We can do something similar in Riverpod, but the syntax is different.
First, in Riverpod, the definition of our UserIdNotifier
would be:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
);
From there, to generate our String
based on the userId
, we could do:
final labelProvider = Provider<String>((ref) {
UserIdNotifier userIdNotifier = ref.watch(userIdNotifierProvider);
return 'The user ID of the the user is ${userIdNotifier.userId}';
});
Notice the line doing ref.watch(userIdNotifierProvider)
.
This line of code tells Riverpod to obtain the content of the userIdNotifierProvider
and that whenever that value changes, labelProvider
will be recomputed too.
As such, the String
emitted by our labelProvider
will automatically update
whenever the userId
changes.
This ref.watch
line should feel similar. This pattern was covered previously
when explaining how to read providers inside widgets.
Indeed, providers are now able to listen to other providers in the same way
that widgets do.
Combining providers: ProxyProvider with stateful objects
When combining providers, another alternative use-case is to expose
stateful objects, such as a ChangeNotifier
instance.
For that, we could use ChangeNotifierProxyProvider
(or variants such as ChangeNotifierProxyProvider2
).
For example we may define:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
Then, we can define a new ChangeNotifier
that is based on UserIdNotifier.userId
.
For example we could do:
class UserNotifier extends ChangeNotifier {
String? _userId;
void setUserId(String? userId) {
if (userId != _userId) {
print('The user ID changed from $_userId to $userId');
_userId = userId;
}
}
}
// ...
ChangeNotifierProxyProvider<UserIdNotifier, UserNotifier>(
create: (context) => UserNotifier(),
update: (context, userIdNotifier, userNotifier) {
return userNotifier!
..setUserId(userIdNotifier.userId);
},
);
This new provider creates a single instance of UserNotifier
(which is never re-constructed)
and prints a string whenever the user ID changes.
Doing the same thing in provider is achieved differently.
First, in Riverpod, the definition of our UserIdNotifier
would be:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
From there, the equivalent to the previous ChangeNotifierProxyProvider
would be:
class UserNotifier extends ChangeNotifier {
String? _userId;
void setUserId(String? userId) {
if (userId != _userId) {
print('The user ID changed from $_userId to $userId');
_userId = userId;
}
}
}
// ...
final userNotifierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
final userNotifier = UserNotifier();
ref.listen<UserIdNotifier>(
userIdNotifierProvider,
(previous, next) {
if (previous?.userId != next.userId) {
userNotifier.setUserId(next.userId);
}
},
);
return userNotifier;
});
The core of this snippet is the ref.listen
line.
This ref.listen
function is a utility that allows listening to a provider,
and whenever the provider changes, executes a function.
The previous
and next
parameters of that function correspond to the
last value before the provider changed and the new value after it changed.