-
-
Notifications
You must be signed in to change notification settings - Fork 956
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reworking the "Riverpod for Provider users" documentation section #2676
Changes from 20 commits
51e7e0b
18a895e
f0850e2
4d4045f
e3d6b10
9bae4a7
cf54d8e
3917f58
5965d81
483fd15
12378b6
af67e8e
6acedde
c68e213
d36bbca
30bcec1
a9498c0
7be7b85
6517102
a4477f1
33a364b
a5dd954
0c4b923
c4ed092
a172779
d817125
bd2acd1
4d6ea01
afa29e2
6b4c400
ed9d746
d804a15
66571e0
ff21c06
3d97a9d
f3a9b8b
c984c29
0b55db4
c61034c
0f4c029
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,62 +18,179 @@ import { | |
This article is designed for people familiar with the [Provider] package who | ||
wants to learn about Riverpod. | ||
|
||
## The relationship between Riverpod and Provider | ||
Since Provider is widely popular, why would one migrate to Riverpod? | ||
|
||
## Provider's limitations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is taking too much focus on the page. Folks mainly look for migration. They probably don't care about the history behind all this. Only a minority do. Maybe this should be moved to a devblog/article, and this section should be restricted to:
This would cleanup this page a bit, without removing information. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docusaurus already enables writing "articles". I've never had the need for this. But this could be a good use-case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know. But keep in mind that #2676 is only a quick draft of what should be covered. I think that's generally Riverpod's issue. There's too much docs :P There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realize this. I'm sure I can be more concise, but at the same time I wrote these subparagraphs while reminding of all the questions we're given into discord daily. So you're telling me that on one hand folks want documentation, and on the other they don't want to read it 😆 While this is true, it all boils down to split this page as suggested below. ALSO some examples (code) might alleviate readability. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The content is great, don't worry about that. I'm mainly worried about formatting, which is not something those GitHub issues care about. Examples would for sure help. Although it could also have the adverse effect (since the page would become an even larger wall of text). I'd combine examples with your proposal of splitting this into subpages |
||
Provider has fundamental issues due to being restricted by the InheritedWidget API. | ||
Inherently, Provider is a "simpler `InheritedWidget`", or, in other words, | ||
Provider is merely an InheritedWidget wrapper, and thus it's limited by it. | ||
|
||
By the end of this paragraph you should be convinced that Riverpod is to be prefered over Provider. | ||
**Riverpod is indeed a more modern, recommended and reliable approach towards State Management, Caching and Reactivty**, | ||
whereas Provider is currently lacking in many areas with no way forward. | ||
|
||
Here's a list of known Provider issues. | ||
|
||
### Provider can't keep two (or more) providers of the same "type" | ||
Declaring two `Provider<int>` will result into unreliable behavior: `InheritedWidget`'s API will | ||
obtain only one of the two: the closest `Provider<int>` 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 having one | ||
`Provider<List<Item>> itemsProvider` for all items, and use it to create e.g. | ||
`Provider<List<Item>> filteredItemsProvider`. | ||
|
||
### 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 requested 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 `AsyncValue`'s APIs. | ||
With Provider, this isn't as easy, or achievable, even. | ||
|
||
### 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. | ||
Instead, 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]. | ||
|
||
### Lack of safety. | ||
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 Riverpod brings much more utility than this, this exception simply can't be thrown. | ||
|
||
### 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]. | ||
Furthermore, these two APIs enable flexible and creative caching strategies (e.g. time-based caching). | ||
Unluckily, there's no way to implement this with raw `InheritedWidgets`, 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. | ||
|
||
### 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]. | ||
|
||
## Towards Riverpod | ||
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 relationship between Riverpod and Provider | ||
Still, conceptually, Riverpod and Provider are fairly similar. | ||
Both packages fill a similar role. Both try to: | ||
|
||
Riverpod is designed to be the spiritual successor of Provider. Hence the | ||
name "Riverpod", which is an anagram of "Provider". | ||
- 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. | ||
|
||
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. | ||
You can think of Riverpod as what Provider could've been if it continued to mature for a few years. | ||
|
||
Still, conceptually, Riverpod and Provider are fairly similar. | ||
Both packages fill a similar role. Both try to: | ||
### The breaking change | ||
The only true downside of Riverpod is that it requires changing the widget type to work: | ||
|
||
- 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. | ||
- Instead of extending `StatelessWidget`, with Riverpod you should extend `ConsumerWidget`. | ||
- Instead of extending `StatefulWidget`, with Riverpod you should extend `ConsumerStatefulWidget`. | ||
|
||
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 overhaul better designed and could lead to drastic simplifications of your logic. | ||
|
||
## Migrating tips | ||
Migrating from Provider to Riverpod can be very straightforward. | ||
Migrating basically consists in a few steps that can be done in an *incremental* way. | ||
|
||
### A gentle start | ||
*If you have an existing app, don't try to migrate all your providers at once.* | ||
Don't immediately try to change your `ChangeNotifiers` into the more modern [AsyncNotifiers] from Riverpod. | ||
|
||
This will requite a bit of a paradigm shift, so it may be difficult to do initially. | ||
Take your time, as it is important to get yourself familiar with Riverpod first. | ||
|
||
It's fine to keep using `ChangeNotifier` using Riverpod and not use the latest fancy Riverpod features ASAP. | ||
|
||
While you should strive toward moving all your application to Riverpod in the long-run, | ||
**don't burn yourself out**. Do it one provider at a time. | ||
|
||
At the same time, think of Riverpod as what Provider could've been if | ||
it continued to mature for a few years. | ||
### Riverpod and Provider can coexist | ||
*Keep in mind that it is entirely possible to use both Provider and Riverpod at the same time.* | ||
|
||
Riverpod fixes various fundamental problems with Provider, such as but not limited to: | ||
Indeed, using import aliases, it is possible to use the two APIs altogheter. | ||
This is also great for readibilty and it removes any ambiguous API usage. | ||
|
||
- 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 | ||
plain `int` or `String` 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. | ||
If you plan on doing this, consider using import aliases for each Provider import in your codebase. | ||
|
||
... And a lot more. | ||
:::info | ||
A full guide onto how to effectively implement import aliases is incoming soon. | ||
::: | ||
|
||
### Migrating one Provider at a time | ||
*All Providers from pkg:provider have a strict equivalent in Riverpod*. | ||
|
||
In particular, you'll find [ChangeNotifierProvider], which is there to enable migration from Provider. | ||
This provider is not recommended when writing new code, and it is not the best way to use Riverpod, but | ||
it's a gentle and very easy way to start your migration. | ||
|
||
The only true downside of Riverpod is that it requires changing the widget type | ||
to work: | ||
Thus, migrating consists in moving a `provider.Provider` definition *outside* of `MultiProvider`, | ||
and change it to a `riverpod.Provider` definition (see the differences below). | ||
|
||
- Instead of extending `StatelessWidget`, with Riverpod you should extend | ||
`ConsumerWidget`. | ||
- Instead of extending `StatefulWidget`, with Riverpod you should extend | ||
`ConsumerStatefulWidget`. | ||
|
||
But this inconvenience is fairly minor in the grand scheme of things. And this | ||
requirement might one day disappear. | ||
### Code Generation | ||
[Code generation] is the recommended way to use Riverpod the *cutting edge* way. | ||
Even so, don't try using codegen right away. | ||
|
||
So to answer the question you're probably asking yourself: | ||
**Should I use Provider or Riverpod?** | ||
Instead, this should be taken as a furhter step forward. | ||
rrousselGit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
You probably should be using Riverpod. | ||
Riverpod is overhaul better designed and could lead to drastic simplifications | ||
of your logic. | ||
Once the "base" migration is done, and you're positive that there are no more `ChangeNotifierProvider`s | ||
in your codebase, you can start migrating towards the codegen apprach, which has a simpler yet different syntax. | ||
|
||
## The difference between Provider and Riverpod | ||
|
||
## Differences and similarities between Provider and Riverpod | ||
This recap should suffice when performing migrations, as it quickly summarizes | ||
defferences and similarities between Provider and Riverpod. | ||
|
||
### Defining providers | ||
|
||
|
@@ -406,7 +523,53 @@ 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. | ||
|
||
### Scoping Providers vs `.family` + `.autoDispose` | ||
In pkg:Provider, scoping was used for two things: | ||
- destroying state when leaving a page | ||
- having custom state per page | ||
|
||
As stated above, using scoping just to destroy state isn't ideal. | ||
The problem is that scoping didn't work well over large applications. | ||
For example, state often is created in one page, but destroyed later in a different page after navigation. | ||
This doesn't allow for multiple caches to be active over different pages. | ||
|
||
Similarly, the "custom state per page" approach quickly becomes difficult to handle if that state | ||
needs to be shared with another part of the widget tree, | ||
like you'd need with modals or a with a multi-step form. | ||
|
||
Riverpod takes a different approach: first, scoping providers is kind of discouraged; second, | ||
`.family` and `.autoDispose` are a complete replacement solution for this. | ||
|
||
Within Riverpod, Providers marked as `.autoDispose` automatically destroy their state when they aren't used anymore. | ||
When the last widget removing a provider is unmounted, Riverpod will detect this and destroy the provider. | ||
|
||
This inherently solves the "destroying state" problem. | ||
|
||
Also it is possible to mark a Provider as `.family` (and, at the same time, as `.autoDispose`). | ||
This enables passing parameters to providers, which make multiple providers to be spawned and tracked internally. | ||
In other words, when passing parameters, *a unique state is created per unique parameter*. | ||
|
||
This solves the "custom state per page" problem. Actually, there's another advantage: such state is no-longer bound to one specific page. | ||
Instead, if a different page tries to access the same state, such page will be able to do so by just reusing the parameters. | ||
|
||
In many ways, passing parameters to providers is equivalent to a Map key. | ||
If the key is the same, the value obtained is the same. If it's a different key, a different state will be obtained. | ||
|
||
[provider]: https://pub.dev/packages/provider | ||
[ref.watch]: /docs/concepts/reading#using-refwatch-to-observe-a-provider | ||
[ref.listen]: /docs/concepts/reading#using-reflisten-to-react-to-a-provider-change | ||
[autodispose]: /docs/concepts/modifiers/auto_dispose | ||
[workaround]: https://pub.dev/packages/provider#can-i-obtain-two-different-providers-using-the-same-type | ||
[.family modifier]: /docs/concepts/modifiers/family | ||
[keepAlive]: /docs/concepts/modifiers/auto_dispose#refkeepalive | ||
[as these two features go hand-in-hand]: /docs/concepts/modifiers/family#prefer-using-autodispose-when-the-parameter-is-not-constant | ||
[simplification of logic]: /docs/concepts/modifiers/family#usage | ||
[we have to]: https://github.com/flutter/flutter/issues/128432 | ||
[it turns out]: https://github.com/flutter/flutter/issues/106549 | ||
[*can't* react when a consumer stops listening to them]: https://github.com/flutter/flutter/issues/106546 | ||
[integrates well with Flutter]: /docs/concepts/reading#using-reflisten-to-react-to-a-provider-change | ||
[ChangeNotifierProvider]: /docs/providers/change_notifier_provider | ||
[Code generation]: /docs/about_code_generation | ||
[AsyncNotifiers]: /docs/providers/notifier_provider | ||
[combining Providers]: /docs/concepts/combining_providers | ||
[global final variable]: /docs/concepts/providers#creating-a-provider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That probably should be a heading instead of the current "Provider's limitations"