Motivation
This in-depth article is meant to show why Riverpod is even a thing.
In particular, this section should answer the following:
- Since Provider is widely popular, why would one migrate to Riverpod?
- What concrete advantages do I get?
- How can I migrate towards Riverpod?
- Can I migrate incrementally?
- etc.
By the end of this section you should be convinced that Riverpod is to be prefered over Provider.
Riverpod is indeed a more modern, recommended and reliable approach when compared to Provider.
Riverpod offers better State Management capabilities, better Caching strategies and a simplified Reactivty model.
Whereas, Provider is currently lacking in many areas with no way forward.
Provider's Limitations
Provider has fundamental issues due to being restricted by the InheritedWidget API.
Inherently, Provider is a "simpler InheritedWidget
";
Provider is merely an InheritedWidget wrapper, and thus it's limited by it.
Here's a list of known Provider issues.
Provider can't keep two (or more) providers of the same "type"
Declaring two Provider<Item>
will result into unreliable behavior: InheritedWidget
's API will
obtain only one of the two: the closest Provider<Item>
ancestor.
While a workaround is explained in Provider's
documentation, Riverpod simply doesn't have this problem.
By removing this limitation, we can freely split logic into tiny pieces, like so:
List<Item> items(Ref ref) {
return []; // ...
}
List<Item> evenItems(Ref ref) {
final items = ref.watch(itemsProvider);
return [...items.whereIndexed((index, element) => index.isEven)];
}
Providers reasonably emit only one value at a time
When reading an external RESTful API, it's quite common to show
the last read value, while a new call loads the next one.
Riverpod allows this behavior via emitting two values at a time (i.e. a previous data value,
and an incoming new loading value), via its AsyncValue
's APIs:
Future<List<Item>> itemsApi(Ref ref) async {
final client = Dio();
final result = await client.get<List<dynamic>>('your-favorite-api');
final parsed = [...result.data!.map((e) => Item.fromJson(e as Json))];
return parsed;
}
List<Item> evenItems(Ref ref) {
final asyncValue = ref.watch(itemsApiProvider);
if (asyncValue.isReloading) return [];
if (asyncValue.hasError) return const [Item(id: -1)];
final items = asyncValue.requireValue;
return [...items.whereIndexed((index, element) => index.isEven)];
}
In the previous snippet, watching evenItemsProvider
will produce the following effects:
- Initially, the request is being made. We obtain an empty list;
- Then, say an error occurs. We obtain
[Item(id: -1)]
; - Then, we retry the request with a pull-to-refresh logic (e.g. via
ref.invalidate
); - While we reload the first provider, the second one still exposes
[Item(id: -1)]
; - This time, some parsed data is received correctly: our even items are correctly returned.
With Provider, the above features aren't remotely achievable, and even less easy to workaround.
Combining providers is hard and error prone
With Provider we may be tempted to use context.watch
inside provider's create
.
This would be unreliable, as didChangeDependencies
may be triggered even if no dependency
has changed (e.g. such as when there's a GlobalKey involved in the widget tree).
Nonetheless, Provider has an ad-hoc solution named ProxyProvider
, but it's considered tedious and error-prone.
Combining state is a core Riverpod mechanism, as we can combine and cache values reactively with zero overhead with simple yet powerful utilites such as ref.watch and ref.listen:
int number(Ref ref) {
return Random().nextInt(10);
}
int doubled(Ref ref) {
final number = ref.watch(numberProvider);
return number * 2;
}
Combining values feels natural with Riverpod: dependencies are readable and the APIs remain the same.
Lack of safety
With Provider, it's common to end-up with a ProviderNotFoundException
during refactors and / or during large changes.
Indeed, this runtime exception was one of the main reasons Riverpod was created in the first place.
Although it brings much more utility than this, Riverpod simply can't throw this exception.
Disposing of state is difficult
InheritedWidget
can't react when a consumer stops listening to them.
This prevents the ability for Provider
to automatically destroy its providers' state when they're no-longer used.
With Provider, we have to rely on scoping providers to dispose the state when it stops being used.
But this isn't easy, as it gets tricky when state is shared between pages.
Riverpod solves this with easy-to-understand APIs such as autodispose and keepAlive.
These two APIs enable flexible and creative caching strategies (e.g. time-based caching):
// With code gen, .autoDispose is the default
int diceRoll(Ref ref) {
// Since this provider is .autoDispose, un-listening to it will dispose
// its current exposed state.
// Then, whenever this provider is listened to again,
// a new dice will be rolled and exposed again.
final dice = Random().nextInt(10);
return dice;
}
int cachedDiceRoll(Ref ref) {
final coin = Random().nextInt(10);
if (coin > 5) throw Exception('Way too large.');
// The above condition might fail;
// If it doesn't, the following instruction tells the Provider
// to keep its cached state, even when no one listens to it anymore.
ref.keepAlive();
return coin;
}
Unluckily, there's no way to implement this with a raw InheritedWidget
, and thus with Provider.
Lack of a reliable parametrization mechanism
Riverpod allows its user to declare "parametrized" Providers with the .family modifier.
Indeed, .family
is one of Riverpod's most powerful feature and it is core to its innovations,
e.g. it enables enormous simplification of logic.
If we wanted to implement something similar using Provider, we would have to give up easiness of use and type-safeness on such parameters.
Furthermore, not being able to implement a similar .autoDispose
mechanism with Provider
inherently prevents any equivalent implementation of .family
, as these two features go hand-in-hand.
Finally, as shown before, it turns out that widgets never stop to listen to an InheritedWidget
.
This implies significant memory leaks if some provider state is "dynamically mounted", i.e. when using parameters
to a build a Provider, which is exactly what .family
does.
Thus, obtaining a .family
equivalent for Provider is fundamentally impossible at the moment in time.
Testing is tedious
To be able to write a test, you have to re-define providers inside each test.
With Riverpod, providers are ready to use inside tests, by default. Furthermore, Riverpod exposes a handy collection of "overriding" utilites that are crucial when mocking Providers.
Testing the combined state snippet above would be as simple as the following:
void main() {
test('it doubles the value correctly', () async {
final container = ProviderContainer(
overrides: [numberProvider.overrideWith((ref) => 9)],
);
final doubled = container.read(doubledProvider);
expect(doubled, 9 * 2);
});
}
For more info about testing, see Testing.
Triggering side effects isn't straightforward
Since InheritedWidget
has no onChange
callback, Provider can't have one.
This is problematic for navigation, such as for snackbars, modals, etc.
Instead, Riverpod simply offers ref.listen
, which integrates well with Flutter.
class DiceRollWidget extends ConsumerWidget {
const DiceRollWidget({super.key});
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(diceRollProvider, (previous, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Dice roll! We got: $next')),
);
});
return TextButton.icon(
onPressed: () => ref.invalidate(diceRollProvider),
icon: const Icon(Icons.casino),
label: const Text('Roll a dice'),
);
}
}
Towards Riverpod
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.
You can think of Riverpod as what Provider could've been if it continued to mature for a few years.
Why a separate package?
Originally, a major version of Provider was planned to ship, as a way to solve
the aforementioned problems.
But it was then decided against it, as this would have been
"too breaking" and even controversial, because of the new ConsumerWidget
API.
Since Provider is still one of the most used Flutter packages, it was instead decided
to create a separate package, and thus Riverpod was created.
Creating a separate package enabled:
- Ease of migration for whoever wants to, by also enabling the temporary use of both approaches, at the same time;
- Allow folks to stick to Provider if they dislike Riverpod in principle, or if they didn't find it reliable yet;
- Experimentation, allowing for Riverpod to search for production-ready solutions to the various Provider's technical limitations.
Indeed, Riverpod is designed to be the spiritual successor of Provider. Hence the name "Riverpod" (which is an anagram of "Provider").
The breaking change
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.
Choosing the right library
You're probably asking yourself: "So, as a Provider user, should I use Provider or Riverpod?".
We want to answer to this question very clearly:
You probably should be using Riverpod
Riverpod is overall better designed and could lead to drastic simplifications of your logic.