Skip to content
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

Merged
merged 40 commits into from
Jul 21, 2023
Merged
Changes from 20 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
51e7e0b
A gentle introduction
lucavenir Jun 24, 2023
18a895e
Provider cant keep same type providers
lucavenir Jun 24, 2023
f0850e2
Provider can't emit multiple values
lucavenir Jun 24, 2023
4d4045f
Provider cant combine state
lucavenir Jun 24, 2023
e3d6b10
Provider isn't safe
lucavenir Jun 24, 2023
9bae4a7
Disposing of state is difficult in Provider
lucavenir Jun 24, 2023
cf54d8e
Provider has no .family
lucavenir Jun 24, 2023
3917f58
Triggering side effects is not cool
lucavenir Jun 24, 2023
5965d81
Rephrasing and restructuring of the introduction
lucavenir Jun 24, 2023
483fd15
Adding links up until now
lucavenir Jun 24, 2023
12378b6
Changed the catching phrase
lucavenir Jun 24, 2023
af67e8e
One full read, one full correction
lucavenir Jun 24, 2023
6acedde
Moved the ProxyProvider section where it belongs
lucavenir Jun 24, 2023
c68e213
Testing is tedious
lucavenir Jun 24, 2023
d36bbca
Finished the introduction section
lucavenir Jun 24, 2023
30bcec1
Improved "same type providers" section
lucavenir Jun 24, 2023
a9498c0
I don't like this title anymore
lucavenir Jun 24, 2023
7be7b85
Added migration tips
lucavenir Jun 24, 2023
6517102
Added a Scoping vs `.family.autoDispose` paragraph
lucavenir Jun 24, 2023
a4477f1
Typo
lucavenir Jun 24, 2023
33a364b
Update website/docs/riverpod_for_provider_users.mdx
rrousselGit Jun 27, 2023
a5dd954
Refactoring
lucavenir Jun 28, 2023
0c4b923
One last dance
lucavenir Jun 29, 2023
c4ed092
Fixed lints and compilation errors
lucavenir Jun 29, 2023
a172779
Re-structured the articles
lucavenir Jul 11, 2023
d817125
Start with `ChangeNotifierProvider`
lucavenir Jul 13, 2023
bd2acd1
Quick hotfixes
lucavenir Jul 13, 2023
4d6ea01
ProxyProvider migration
lucavenir Jul 13, 2023
afa29e2
Moved the "cohexist" section down
lucavenir Jul 13, 2023
6b4c400
Start with the leaves section
lucavenir Jul 13, 2023
ed9d746
No need to use Consumer right away
lucavenir Jul 13, 2023
d804a15
About eager initialization
lucavenir Jul 13, 2023
66571e0
There's no `ConsumerN`
lucavenir Jul 13, 2023
ff21c06
watch.select VS ref.watch(provider.select)
lucavenir Jul 13, 2023
3d97a9d
Re-arranged the quickstart again
lucavenir Jul 13, 2023
f3a9b8b
Hotfix
lucavenir Jul 13, 2023
c984c29
Added the codegen guide for ChangeNotifierProviders
lucavenir Jul 17, 2023
0b55db4
Merge branch 'rework-flow' into docs-v2
lucavenir Jul 17, 2023
c61034c
Changed the reading flow of the motivation page
lucavenir Jul 17, 2023
0f4c029
Merge branch 'rework-flow' of https://github.com/rrousselGit/riverpod…
rrousselGit Jul 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 205 additions & 42 deletions website/docs/riverpod_for_provider_users.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Owner

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"


## Provider's limitations
Copy link
Owner

Choose a reason for hiding this comment

The 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:

  • a simple list of the various points
  • a link to the detailed devblog

This would cleanup this page a bit, without removing information.

Copy link
Owner

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've wrote all of these limitations in response of #2629 (see bullet points). I interpreted those bullet points as "necessary" to fix #2629. Moving "Provider's limitations" into a separate page makes sense to me

Copy link
Owner

Choose a reason for hiding this comment

The 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.
In the end it's quite important to make the page readable. Otherwise people will simply not read it, because they are scared of walls of texts.

I think that's generally Riverpod's issue. There's too much docs :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Owner

Choose a reason for hiding this comment

The 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

Expand Down Expand Up @@ -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