website/docs/migration/from_state_notifier.mdx
import buildInit from "./from_state_notifier/build_init"; import buildInitOld from "!!raw-loader!./from_state_notifier/build_init_old.dart"; import consumersDontChange from "!!raw-loader!./from_state_notifier/consumers_dont_change.dart"; import familyAndDispose from "./from_state_notifier/family_and_dispose"; import familyAndDisposeOld from "!!raw-loader!./from_state_notifier/family_and_dispose_old.dart"; import asyncNotifier from "./from_state_notifier/async_notifier"; import asyncNotifierOld from "!!raw-loader!./from_state_notifier/async_notifier_old.dart"; import addListener from "./from_state_notifier/add_listener"; import addListenerOld from "!!raw-loader!./from_state_notifier/add_listener_old.dart"; import fromStateProvider from "./from_state_notifier/from_state_provider"; import fromStateProviderOld from "!!raw-loader!./from_state_notifier/from_state_provider_old.dart"; import oldLifecycles from "./from_state_notifier/old_lifecycles"; import oldLifecyclesOld from "!!raw-loader!./from_state_notifier/old_lifecycles_old.dart"; import oldLifecyclesFinal from "./from_state_notifier/old_lifecycles_final"; import obtainNotifierOnTests from "!!raw-loader!./from_state_notifier/obtain_notifier_on_tests.dart";
import { Link } from "/src/components/Link"; import { AutoSnippet } from "/src/components/CodeSnippet";
Along with Riverpod 2.0, new classes
were introduced: Notifier / AsyncNotifier.
StateNotifier is now discouraged in favor of those new APIs.
This page shows how to migrate from the deprecated StateNotifier to the new APIs.
The main benefit introduced by AsyncNotifier is a better async support; indeed,
AsyncNotifier can be thought as a FutureProvider which can expose ways to be modified from the UI.
Furthermore, the new (Async)Notifiers:
Ref object inside its classLet's see how to define a Notifier, how it compares with StateNotifier and how to migrate
the new AsyncNotifier for asynchronous state.
Be sure to know how to define a Notifier before diving into this comparison.
See <Link documentID="concepts2/providers" />.
Let's write an example, using the old StateNotifier syntax:
<AutoSnippet raw={buildInitOld}/>
Here's the same example, built with the new Notifier APIs, which roughly translates to:
<AutoSnippet language="dart" {...buildInit}></AutoSnippet>
Comparing Notifier with StateNotifier, one can observe these main differences:
StateNotifier's reactive dependencies are declared in its provider, whereas Notifier
centralizes this logic in its build methodStateNotifier's whole initialization process is split between its provider and its constructor,
whereas Notifier reserves a single place to place such logicStateNotifier, no logic is ever written into a Notifier's constructorSimilar conclusions can be made with AsyncNotifier, Notifier's asynchronous equivalent.
StateNotifiersThe main appeal of the new API syntax is an improved DX on asynchronous data.
Take the following example:
Here's the above example, rewritten with the new AsyncNotifier APIs:
<AutoSnippet language="dart" {...asyncNotifier}></AutoSnippet>
AsyncNotifier, just like Notifier, brings a simpler and more uniform API.
Here, it's easy to see AsyncNotifier as a FutureProvider with methods.
AsyncNotifier comes with a set of utilities and getters that StateNotifier doesn't have, such as e.g.
future
and update.
This enables us to write much simpler logic when handling asynchronous mutations and side-effects.
See also <Link documentID="concepts2/providers" />.
:::tip
Migrating from StateNotifier<AsyncValue<T>> to a AsyncNotifier<T> boils down to:
buildcatch/try blocks in initialization or in side effects methodsAsyncValue.guard from build, as it converts Futures into AsyncValues
:::After these few examples, let's now highlight the main advantages of Notifier and AsyncNotifier:
Let's go further down and highlight more differences and similarities.
.family and .autoDispose modificationsAnother important difference is how families and auto dispose is handled with the new APIs.
Modifications are explicitly stated inside the class; any parameters are directly injected in the
build method, so that they're available to the initialization logic.
This should bring better readability, more conciseness and overall less mistakes.
Take the following example, in which a StateNotifierProvider.family is being defined.
<AutoSnippet raw={familyAndDisposeOld}/>
BugsEncounteredNotifier feels... heavy / hard to read.
Let's take a look at its migrated AsyncNotifier counterpart:
<AutoSnippet language="dart" {...familyAndDispose}></AutoSnippet>
Its migrated counterpart should feel like a light read.
:::info
(Async)Notifier's .family parameters are available via this.arg (or this.paramName when using codegen)
:::
Lifecycles between Notifier/AsyncNotifier and StateNotifier differ substantially.
This example showcases - again - how the old API have sparse logic:
<AutoSnippet raw={oldLifecyclesOld}/>Here, if durationProvider updates, MyNotifier disposes: its instance is then re-instantiated
and its internal state is then re-initialized.
Furthermore, unlike every other provider, the dispose callback is to be defined
in the class, separately.
Finally, it is still possible to write ref.onDispose in its provider, showing once again how
sparse the logic can be with this API; potentially, the developer might have to look into eight (8!)
different places to understand this Notifier behavior!
These ambiguities are solved with Riverpod 2.0.
dispose vs ref.onDisposeStateNotifier's dispose method refers to the dispose event of the notifier itself, aka it's a
callback that gets called before disposing of itself.
(Async)Notifiers don't have this property, since they don't get disposed of on rebuild; only
their internal state is.
In the new notifiers, dispose lifecycles are taken care of in only one place, via ref.onDispose
(and others), just like any other provider.
This simplifies the API, and hopefully the DX, so that there is only one place to look at to
understand lifecycle side-effects: its build method.
Shortly: to register a callback that fires before its internal state rebuilds, we can use
ref.onDispose like every other provider.
You can migrate the above snippet like so:
<AutoSnippet language="dart" {...oldLifecycles}></AutoSnippet>
In this last snippet there sure is some simplification, but there's still an open problem: we
are now unable to understand whether or not our notifiers are still alive while performing update.
This might arise an unwanted StateErrors.
mountedThis happens because (Async)Notifiers lacks a mounted property, which was available on
StateNotifier.
Considering their difference in lifecycle, this makes perfect sense; while possible, a mounted
property would be misleading on the new notifiers: mounted would almost always be true.
While it would be possible to craft a custom workaround, it's recommended to work around this by canceling the asynchronous operation.
Canceling an operation can be done with a custom Completer, or any custom derivative.
For example, if you're using Dio to perform network requests, consider using a cancel token
(see also <Link documentID="concepts2/auto_dispose" />).
Therefore, the above example migrates to the following: <AutoSnippet language="dart" {...oldLifecyclesFinal}></AutoSnippet>
Up until now we've shown the differences between StateNotifier and the new APIs.
Instead, one thing Notifier, AsyncNotifier and StateNotifier share is how their states
can be consumed and mutated.
Consumers can obtain data from these three providers with the same syntax, which is great in case
you're migrating away from StateNotifier; this applies for notifiers methods, too.
<AutoSnippet raw={consumersDontChange}></AutoSnippet>
Let's explore the less-impactful differences between StateNotifier and Notifier (or AsyncNotifier)
.addListener and .streamStateNotifier's .addListener and .stream can be used to listen for state changes.
These two APIs are now to be considered outdated.
This is intentional due to the desire to reach full API uniformity with Notifier, AsyncNotifier and other providers.
Indeed, using a Notifier or an AsyncNotifier shouldn't be any different from any other provider.
Therefore this: <AutoSnippet raw={addListenerOld}/>
Becomes this: <AutoSnippet language="dart" {...addListener}></AutoSnippet>
In a nutshell: if you want to listen to a Notifier/AsyncNotifier, just use ref.listen.
See <Link documentID="concepts2/refs" />.
.debugState in testsStateNotifier exposes .debugState: this property is used for pkg:state_notifier users to enable
state access from outside the class when in development mode, for testing purposes.
If you're using .debugState to access state in tests, chances are that you need to drop this
approach.
Notifier / AsyncNotifier don't have a .debugState; instead, they directly expose .state,
which is @visibleForTesting.
:::danger
AVOID accessing .state from tests; if you have to, do it if and only if you had already have
a Notifier / AsyncNotifier properly instantiated;
then, you could access .state inside tests freely.
Indeed, Notifier / AsyncNotifier should not be instantiated by hand; instead, they should be
interacted with by using its provider: failing to do so will break the notifier,
due to ref and family args not being initialized.
:::
Don't have a Notifier instance?
No problem, you can obtain one with ref.read, just like you would read its exposed state:
Learn more about testing in its dedicated guide. See <Link documentID="how_to/testing" />.
StateProviderStateProvider was exposed by Riverpod since its release, and it was made to save a few LoC for
simplified versions of StateNotifierProvider.
Since StateNotifierProvider is deprecated, StateProvider is to be avoided, too.
Furthermore, as of now, there is no StateProvider equivalent for the new APIs.
Nonetheless, migrating from StateProvider to Notifier is simple.
This: <AutoSnippet raw={fromStateProviderOld}/>
Becomes: <AutoSnippet language="dart" {...fromStateProvider}></AutoSnippet>
Even though it costs us a few more LoC, migrating away from StateProvider enables us to
archive StateNotifier.