diff --git a/website/docs/migration/0.14.0_to_1.0.0.mdx b/website/docs/migration/0.14.0_to_1.0.0.mdx index f7254f460..37320ec1d 100644 --- a/website/docs/migration/0.14.0_to_1.0.0.mdx +++ b/website/docs/migration/0.14.0_to_1.0.0.mdx @@ -2,6 +2,9 @@ title: ^0.14.0 to ^1.0.0 --- +import { Link } from "../../src/components/Link"; + + After a long wait, the first stable version of Riverpod is finally released 👏 To see the full list of changes, consult the [Changelog](https://pub.dev/packages/flutter_riverpod/changelog#100). diff --git a/website/docs/migration/from_change_notifier.mdx b/website/docs/migration/from_change_notifier.mdx new file mode 100644 index 000000000..9af75d89e --- /dev/null +++ b/website/docs/migration/from_change_notifier.mdx @@ -0,0 +1,131 @@ +--- +title: From `ChangeNotifier` +--- + +import old from "!!raw-loader!./from_change_notifier/old.dart"; +import declaration from "./from_change_notifier/declaration"; +import initialization from "./from_change_notifier/initialization"; +import migrated from "./from_change_notifier/migrated"; + +import { Link } from "../../src/components/Link"; +import { AutoSnippet } from "../../src/components/CodeSnippet"; + + +Within Riverpod, `ChangeNotifierProvider` is meant to be used to offer a smooth transition from +pkg:provider. + +If you've just started a migration to pkg:riverpod, make sure you read the dedicated guide +(see ). +This article is meant for folks that already transitioned to riverpod, but want to move away from +`ChangeNotifier` definetively. + +All in all, migrating from `ChangeNotifier` to `AsyncNotifer` requires a +paradigm shift, but it brings great simplification with the resulting migrated +code. See also . + +Take this (faulty) example: + + +This implementation shows several weak design choices such as: +- The usage of `isLoading` and `hasError` to handle different asynchronous cases +- The need to carefully handle requests with tedious `try`/`catch`/`finally` expressions +- The need to inkove `notifyListeners` at the right times to make this implementation work +- The presence of inconsistent or possibly undesirable states, e.g. initialization with an empty list + +Note how this example has been crafted to show how `ChangeNotifier` can lead to faulty design choices +for newbie developers; also, another takeaway is that mutable state might be way harder than it +initially promises. + +`Notifier`/`AsyncNotifer`, in combination with immutable state, can lead to better design choices +and less errors. + +Let's see how to migrate the above snippet, one step at a time, towards the newest APIs. + + +## Start your migration +First, we should declare the new provider / notifier: this requires some thought process which +depends on your unique business logic. + +Let's summarize the above requirements: +- State is represented with `List`, which obtained via a network call, with no parameters +- State should *also* expose info about its `loading`, `error` and `data` state +- State can be mutated via some exposed methods, thus a function isn't enough + +:::tip +The above thought process boils down to answering the following questions: +1. Are some side effects required? + - `y`: Use riverpod's class-based API + - `n`: Use riverpod's function-based API +2. Does state need to be loaded asynchronously? + - `y`: Let `build` return a `Future` + - `n`: Let `build` simply return `T` +3. Are some parameters required? + - `y`: Let `build` (or your function) accept them + - `n`: Let `build` (or your function) accept no extra parameters +::: + +:::info +If you're using codegen, the above thought process is enough. +There's no need to think about the right class names and their *specific* APIs. +`@riverpod` only asks you to write a class with its return type, and you're good to go. +::: + +Technically, the best fit here is to define a `AutoDisposeAsyncNotifier>`, +which meets all the above requirements. Let's write some pseudocode first. + + + +:::tip +Remember: use snippets in your IDE to get some guidance, or just to speed up your code writing. +See . +::: + +With respect with `ChangeNotifier`'s implementation, we don't need to declare `todos` anymore; +such variable is `state`, which is implicitly loaded with `build`. + +Indeed, riverpod's notifiers can expose *one* entity at a time. + +:::tip +Riverpod's API is meant to be granular; nonetheless, when migrating, you can still define a custom +entity to hold multiple values. Consider using [Dart 3's records](https://dart.dev/language/records) +to smooth out the migration at first. +::: + + +### Initialization +Initalizing a notifier is easy: just write initialization logic inside `build`. +We can now get rid of the old `_init` function. + + + +With respect of the old `_init`, the new `build` isn't missing anything: there is no need to +initialize variables such as `isLoading` or `hasError` anymore. + +Riverpod will automatically translate any asynchronous provider, via exposing an `AsyncValue>` +and handles the intricacies of asynchronous state way better than what two simple boolean flags can do. + +Indeed, any `AsyncNotifier` effectively makes writing additional `try`/`catch`/`finally` an anti-pattern +for handling asynchronous state. + + +### Mutations and Side Effects +Just like initialization, when performing side effects there's no need to manipulate boolean flags +such as `hasError`, or to write additional `try`/`catch`/`finally` blocks. + +Below, we've cut down all the boilerplate and successfully fully migrated the above example: + + +:::tip +Syntax and design choices may vary, but in the end we just need to write our request and update +state afterwards. See . +::: + +## Migration Process Summary + +Let's review the whole migration process applied above, from a operational point of view. + +1. We've moved the initialization, away from a custom method invoked in a constructor, to `build` +2. We've removed `todos`, `isLoading` and `hasError` properties: internal `state` will suffice +3. We've removed any `try`-`catch`-`finally` blocks: returning the future is enough +4. We've applied the same simplification on the side effects (`addTodo`) +5. We've applied the mutations, via simply reassign `state` diff --git a/website/docs/migration/from_change_notifier/declaration/declaration.dart b/website/docs/migration/from_change_notifier/declaration/declaration.dart new file mode 100644 index 000000000..b3ca06ef2 --- /dev/null +++ b/website/docs/migration/from_change_notifier/declaration/declaration.dart @@ -0,0 +1,33 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'declaration.g.dart'; + +class Todo { + const Todo(this.id); + Todo.fromJson(Object obj) : id = 0; + + final int id; +} + +class Http { + Future> get(String str) async => [str]; + Future> post(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +@riverpod +class MyNotifier extends _$MyNotifier { + @override + FutureOr> build() { + // TODO ... + return []; + } + + Future addTodo(Todo todo) async { + // TODO + } +} diff --git a/website/docs/migration/from_change_notifier/declaration/declaration.g.dart b/website/docs/migration/from_change_notifier/declaration/declaration.g.dart new file mode 100644 index 000000000..317354e31 --- /dev/null +++ b/website/docs/migration/from_change_notifier/declaration/declaration.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'declaration.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myNotifierHash() => r'fc9a07f8ef9f792da2ac660d76ea0a809335ba18'; + +/// See also [MyNotifier]. +@ProviderFor(MyNotifier) +final myNotifierProvider = + AutoDisposeAsyncNotifierProvider>.internal( + MyNotifier.new, + name: r'myNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$myNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MyNotifier = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_change_notifier/declaration/index.tsx b/website/docs/migration/from_change_notifier/declaration/index.tsx new file mode 100644 index 000000000..1ad659c31 --- /dev/null +++ b/website/docs/migration/from_change_notifier/declaration/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./declaration.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_change_notifier/declaration/raw.dart b/website/docs/migration/from_change_notifier/declaration/raw.dart new file mode 100644 index 000000000..c7485bbba --- /dev/null +++ b/website/docs/migration/from_change_notifier/declaration/raw.dart @@ -0,0 +1,33 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +class Todo { + const Todo(this.id); + Todo.fromJson(Object obj) : id = 0; + + final int id; +} + +class Http { + Future> get(String str) async => [str]; + Future> post(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +@riverpod +class MyNotifier extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() { + // TODO ... + return []; + } + + Future addTodo(Todo todo) async { + // TODO + } +} + +final myNotifierProvider = AsyncNotifierProvider.autoDispose(MyNotifier.new); diff --git a/website/docs/migration/from_change_notifier/initialization/index.tsx b/website/docs/migration/from_change_notifier/initialization/index.tsx new file mode 100644 index 000000000..3b3fbd2cb --- /dev/null +++ b/website/docs/migration/from_change_notifier/initialization/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./initialization.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_change_notifier/initialization/initialization.dart b/website/docs/migration/from_change_notifier/initialization/initialization.dart new file mode 100644 index 000000000..4da805e13 --- /dev/null +++ b/website/docs/migration/from_change_notifier/initialization/initialization.dart @@ -0,0 +1,29 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'initialization.g.dart'; + +class Todo { + const Todo(this.id); + Todo.fromJson(Object obj) : id = 0; + + final int id; +} + +class Http { + Future> get(String str) async => [str]; + Future> post(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +@riverpod +class MyNotifier extends _$MyNotifier { + @override + FutureOr> build() async { + final json = await http.get('api/todos'); + return [...json.map(Todo.fromJson)]; + } +} diff --git a/website/docs/migration/from_change_notifier/initialization/initialization.g.dart b/website/docs/migration/from_change_notifier/initialization/initialization.g.dart new file mode 100644 index 000000000..376ce998f --- /dev/null +++ b/website/docs/migration/from_change_notifier/initialization/initialization.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'initialization.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myNotifierHash() => r'1c67c12443102cf8c43efbf6c630d3028d9847c3'; + +/// See also [MyNotifier]. +@ProviderFor(MyNotifier) +final myNotifierProvider = + AutoDisposeAsyncNotifierProvider>.internal( + MyNotifier.new, + name: r'myNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$myNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MyNotifier = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_change_notifier/initialization/raw.dart b/website/docs/migration/from_change_notifier/initialization/raw.dart new file mode 100644 index 000000000..24ab265f7 --- /dev/null +++ b/website/docs/migration/from_change_notifier/initialization/raw.dart @@ -0,0 +1,29 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +class Todo { + const Todo(this.id); + Todo.fromJson(Object obj) : id = 0; + + final int id; +} + +class Http { + Future> get(String str) async => [str]; + Future> post(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +@riverpod +class MyNotifier extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() async { + final json = await http.get('api/todos'); + return [...json.map(Todo.fromJson)]; + } +} + +final myNotifierProvider = AsyncNotifierProvider.autoDispose(MyNotifier.new); diff --git a/website/docs/migration/from_change_notifier/migrated/index.tsx b/website/docs/migration/from_change_notifier/migrated/index.tsx new file mode 100644 index 000000000..075bfbdf5 --- /dev/null +++ b/website/docs/migration/from_change_notifier/migrated/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./migrated.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_change_notifier/migrated/migrated.dart b/website/docs/migration/from_change_notifier/migrated/migrated.dart new file mode 100644 index 000000000..b8f4ba829 --- /dev/null +++ b/website/docs/migration/from_change_notifier/migrated/migrated.dart @@ -0,0 +1,37 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'migrated.g.dart'; + +class Todo { + const Todo(this.id); + Todo.fromJson(Object obj) : id = 0; + + final int id; +} + +class Http { + Future> get(String str) async => [str]; + Future> post(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +@riverpod +class MyNotifier extends _$MyNotifier { + @override + FutureOr> build() async { + final json = await http.get('api/todos'); + + return [...json.map(Todo.fromJson)]; + } + + Future addTodo(Todo todo) async { + // optional: state = const AsyncLoading(); + final json = await http.post('api/todos'); + final newTodos = [...json.map(Todo.fromJson)]; + state = AsyncData(newTodos); + } +} diff --git a/website/docs/migration/from_change_notifier/migrated/migrated.g.dart b/website/docs/migration/from_change_notifier/migrated/migrated.g.dart new file mode 100644 index 000000000..8eaddcfd9 --- /dev/null +++ b/website/docs/migration/from_change_notifier/migrated/migrated.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'migrated.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myNotifierHash() => r'bde95c56aa12eff7c8c01ede57ae4ad2b616c225'; + +/// See also [MyNotifier]. +@ProviderFor(MyNotifier) +final myNotifierProvider = + AutoDisposeAsyncNotifierProvider>.internal( + MyNotifier.new, + name: r'myNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$myNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MyNotifier = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_change_notifier/migrated/raw.dart b/website/docs/migration/from_change_notifier/migrated/raw.dart new file mode 100644 index 000000000..8572db71d --- /dev/null +++ b/website/docs/migration/from_change_notifier/migrated/raw.dart @@ -0,0 +1,37 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +class Todo { + const Todo(this.id); + Todo.fromJson(Object obj) : id = 0; + + final int id; +} + +class Http { + Future> get(String str) async => [str]; + Future> post(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +@riverpod +class MyNotifier extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() async { + final json = await http.get('api/todos'); + + return [...json.map(Todo.fromJson)]; + } + + Future addTodo(Todo todo) async { + // optional: state = const AsyncLoading(); + final json = await http.post('api/todos'); + final newTodos = [...json.map(Todo.fromJson)]; + state = AsyncData(newTodos); + } +} + +final myNotifierProvider = AsyncNotifierProvider.autoDispose(MyNotifier.new); diff --git a/website/docs/migration/from_change_notifier/old.dart b/website/docs/migration/from_change_notifier/old.dart new file mode 100644 index 000000000..4e65e823d --- /dev/null +++ b/website/docs/migration/from_change_notifier/old.dart @@ -0,0 +1,60 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class Todo { + const Todo(this.id); + Todo.fromJson(Object obj) : id = 0; + + final int id; +} + +class Http { + Future> get(String str) async => [str]; + Future> post(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +class MyChangeNotifier extends ChangeNotifier { + MyChangeNotifier() { + _init(); + } + List todos = []; + bool isLoading = true; + bool hasError = false; + + Future _init() async { + try { + final json = await http.get('api/todos'); + todos = [...json.map(Todo.fromJson)]; + } on Exception { + hasError = true; + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future addTodo(int id) async { + isLoading = true; + notifyListeners(); + + try { + final json = await http.post('api/todos'); + todos = [...json.map(Todo.fromJson)]; + hasError = false; + } on Exception { + hasError = true; + } finally { + isLoading = false; + notifyListeners(); + } + } +} + +final myChangeProvider = ChangeNotifierProvider((ref) { + return MyChangeNotifier(); +}); diff --git a/website/docs/migration/from_state_notifier.mdx b/website/docs/migration/from_state_notifier.mdx new file mode 100644 index 000000000..b2ba50348 --- /dev/null +++ b/website/docs/migration/from_state_notifier.mdx @@ -0,0 +1,264 @@ +--- +title: From `StateNotifier` +--- + +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](https://pub.dev/packages/flutter_riverpod/changelog#200), new classes +were introduced: `Notifier` / `AsyncNotifer`. +`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)Notifier`s: + +- Expose a `Ref` object inside its class +- Offer similar syntax between codegen and non-codegen approaches +- Offer similar syntax between their sync and async versions +- Move away logic from Providers and centralize it into the Notifiers themselves + +Let's see how to define a `Notifier`, how it compares with `StateNotifier` and how to migrate +the new `AsyncNotifier` for asynchronous state. + +## New syntax comparison + +Be sure to know how to define a `Notifier` before diving into this comparison. +See . + +Let's write an example, using the old `StateNotifier` syntax: + + +Here's the same example, built with the new `Notifier` APIs, which roughly translates to: + + +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` method +- `StateNotifier`'s whole initialization process is split between its provider and its constructor, + whereas `Notifier` reserves a single place to place such logic +- Notice how, as opposed to `StateNotifier`, no logic is ever written into a `Notifier`'s constructor + +Similar conclusions can be made with `AsyncNotifer`, `Notifier`'s asynchronous equivalent. + +## Migrating asynchronous `StateNotifier`s + +The 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: + + + +`AsyncNotifer`, just like `Notifier`, brings a simpler and more uniform API. +Here, it's easy to see `AsyncNotifer` as a `FutureProvider` with methods. + +`AsyncNotifer` comes with a set of utilities and getters that `StateNotifier` doesn't have, such as e.g. +[`future`](https://pub.dev/documentation/riverpod/latest/riverpod/AutoDisposeAsyncNotifier/future.html) +and [`update`](https://pub.dev/documentation/riverpod/latest/riverpod/AutoDisposeAsyncNotifier/update.html). +This enables us to write much simpler logic when handling asynchronous mutations and side-effects. +See also . +:::tip +Migrating from `StateNotifier>` to a `AsyncNotifer` boils down to: + +- Putting initialization logic into `build` +- Removing any `catch`/`try` blocks in initialization or in side effects methods +- Remove any `AsyncValue.guard` from `build`, as it converts `Future`s into `AsyncValue`s +::: + + +### Advantages + +After these few examples, let's now highligh the main advantages of `Notifier` and `AsyncNotifer`: +- The new syntax should feel way simpler and more readable, especially for asynchronous state +- New APIs are likely to have less boilerplate code in general +- Syntax is now unified, no matter the type of provider you're writing, enabling code generation +(see ) + +Let's go further down and highlight more differences and similarities. + +## Explicit `.family` and `.autoDispose` modifications + +Another important difference is how families and auto dispose is handled with the new APIs. + +`Notifier`, has its own `.family` and `.autoDispose` counterparts, such as `FamilyNotifier` +and `AutoDisposeNotifier`. +As always, such modifications hese can be combined (aka `AutoDisposeFamilyNotifier`). +`AsyncNotifer` has its asynchronous equivalent, too (e.g. `AutoDisposeFamilyAsyncNotifier`). + +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. + + +`BugsEncounteredNotifier` feels... heavy / hard to read. +Let's take a look at its migrated `AsyncNotifier` counterpart: + + + +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 have a different behavior + +Lifecycles between `Notifier`/`AsyncNotifier` and `StateNotifier` differ substantially. + +This example showcases - again - how the old API have sparse logic: + + + +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`. + +### Old `dispose` vs `ref.onDispose` +`StateNotifier`'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)Notifier`s 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: + + + +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 `StateError`s. + +### No more `mounted` +This happens because `(Async)Notifier`s 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](https://github.com/rrousselGit/riverpod/issues/1879#issuecomment-1303189191), +it's recomended to work around this by canceling the asynchronous operation. + +Canceling an operation can be done with a custom [Completer](https://api.flutter.dev/flutter/dart-async/Completer-class.html), +or any custom derivative. + +For example, if you're using `Dio` to perform network requests, consider using a [cancel token](https://pub.dev/documentation/dio/latest/dio/CancelToken-class.html) +(see also ). + +Therefore, the above example migrates to the following: + + + +## Mutations APIs are the same as before + +Up until now we've shown the differences between `StateNotifier` and the new APIs. +Instad, one thing `Notifier`, `AsyncNotifer` 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. + + + +## Other migrations + +Let's explore the less-impactful differences between `StateNotifier` and `Notifier` (or `AsyncNotifier`) + +### From `.addListener` and `.stream` + +`StateNotifier`'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: + + +Becomes this: + + +In a nutshell: if you want to listen to a `Notifier`/`AsyncNotifer`, just use `ref.listen`. +See . + +### From `.debugState` in tests + +`StateNotifier` 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` / `AsyncNotifer` 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` / `AsyncNotifer` properly instantied; +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 . + +### From `StateProvider` + +`StateProvider` 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: + + +Becomes: + + +Even though it costs us a few more LoC, migrating away from `StateProvider` enables us to +definetively archive `StateNotifier`. diff --git a/website/docs/migration/from_state_notifier/add_listener/add_listener.dart b/website/docs/migration/from_state_notifier/add_listener/add_listener.dart new file mode 100644 index 000000000..ead1c72d8 --- /dev/null +++ b/website/docs/migration/from_state_notifier/add_listener/add_listener.dart @@ -0,0 +1,18 @@ +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'add_listener.g.dart'; + +/* SNIPPET START */ +@riverpod +class MyNotifier extends _$MyNotifier { + @override + int build() { + ref.listenSelf((_, next) => debugPrint('$next')); + return 0; + } + + void add() => state++; +} diff --git a/website/docs/migration/from_state_notifier/add_listener/add_listener.g.dart b/website/docs/migration/from_state_notifier/add_listener/add_listener.g.dart new file mode 100644 index 000000000..161b3bba0 --- /dev/null +++ b/website/docs/migration/from_state_notifier/add_listener/add_listener.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'add_listener.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myNotifierHash() => r'9acd382ed579c545ace755687b155e28eba01d22'; + +/// See also [MyNotifier]. +@ProviderFor(MyNotifier) +final myNotifierProvider = + AutoDisposeNotifierProvider.internal( + MyNotifier.new, + name: r'myNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$myNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MyNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_state_notifier/add_listener/index.tsx b/website/docs/migration/from_state_notifier/add_listener/index.tsx new file mode 100644 index 000000000..6d7ac6d37 --- /dev/null +++ b/website/docs/migration/from_state_notifier/add_listener/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./add_listener.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_state_notifier/add_listener/raw.dart b/website/docs/migration/from_state_notifier/add_listener/raw.dart new file mode 100644 index 000000000..e25b4a181 --- /dev/null +++ b/website/docs/migration/from_state_notifier/add_listener/raw.dart @@ -0,0 +1,17 @@ +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +/* SNIPPET START */ +class MyNotifier extends Notifier { + @override + int build() { + ref.listenSelf((_, next) => debugPrint('$next')); + return 0; + } + + void add() => state++; +} + +final myNotifierProvider = NotifierProvider(MyNotifier.new); diff --git a/website/docs/migration/from_state_notifier/add_listener_old.dart b/website/docs/migration/from_state_notifier/add_listener_old.dart new file mode 100644 index 000000000..5934a0f88 --- /dev/null +++ b/website/docs/migration/from_state_notifier/add_listener_old.dart @@ -0,0 +1,24 @@ +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/* SNIPPET START */ +class MyNotifier extends StateNotifier { + MyNotifier() : super(0); + + void add() => state++; +} + +final myNotifierProvider = StateNotifierProvider((ref) { + final notifier = MyNotifier(); + + final cleanup = notifier.addListener((state) => debugPrint('$state')); + ref.onDispose(cleanup); + + // Or, equivalently: + // final listener = notifier.stream.listen((event) => debugPrint('$event')); + // ref.onDispose(listener.cancel); + + return notifier; +}); diff --git a/website/docs/migration/from_state_notifier/async_notifier/async_notifier.dart b/website/docs/migration/from_state_notifier/async_notifier/async_notifier.dart new file mode 100644 index 000000000..5ead76378 --- /dev/null +++ b/website/docs/migration/from_state_notifier/async_notifier/async_notifier.dart @@ -0,0 +1,28 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'async_notifier.g.dart'; + +class Todo { + Todo.fromJson(Object obj); +} + +class Http { + Future> get(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +@riverpod +class AsyncTodosNotifier extends _$AsyncTodosNotifier { + @override + FutureOr> build() async { + final json = await http.get('api/todos'); + + return [...json.map(Todo.fromJson)]; + } + + // ... +} diff --git a/website/docs/migration/from_state_notifier/async_notifier/async_notifier.g.dart b/website/docs/migration/from_state_notifier/async_notifier/async_notifier.g.dart new file mode 100644 index 000000000..25bcda60d --- /dev/null +++ b/website/docs/migration/from_state_notifier/async_notifier/async_notifier.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'async_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$asyncTodosNotifierHash() => + r'10207327c7dee180e9da8beece5bfffedcf86e98'; + +/// See also [AsyncTodosNotifier]. +@ProviderFor(AsyncTodosNotifier) +final asyncTodosNotifierProvider = + AutoDisposeAsyncNotifierProvider>.internal( + AsyncTodosNotifier.new, + name: r'asyncTodosNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$asyncTodosNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AsyncTodosNotifier = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_state_notifier/async_notifier/index.tsx b/website/docs/migration/from_state_notifier/async_notifier/index.tsx new file mode 100644 index 000000000..a0ff513c3 --- /dev/null +++ b/website/docs/migration/from_state_notifier/async_notifier/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./async_notifier.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_state_notifier/async_notifier/raw.dart b/website/docs/migration/from_state_notifier/async_notifier/raw.dart new file mode 100644 index 000000000..52da61d76 --- /dev/null +++ b/website/docs/migration/from_state_notifier/async_notifier/raw.dart @@ -0,0 +1,29 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +class Todo { + Todo.fromJson(Object obj); +} + +class Http { + Future> get(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +class AsyncTodosNotifier extends AsyncNotifier> { + @override + FutureOr> build() async { + final json = await http.get('api/todos'); + + return [...json.map(Todo.fromJson)]; + } + + // ... +} + +final asyncTodosNotifier = AsyncNotifierProvider>( + AsyncTodosNotifier.new, +); diff --git a/website/docs/migration/from_state_notifier/async_notifier_old.dart b/website/docs/migration/from_state_notifier/async_notifier_old.dart new file mode 100644 index 000000000..50d7f4aed --- /dev/null +++ b/website/docs/migration/from_state_notifier/async_notifier_old.dart @@ -0,0 +1,30 @@ +// ignore_for_file: avoid_print, avoid_unused_constructor_parameters + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class Todo { + Todo.fromJson(Object obj); +} + +class Http { + Future> get(String str) async => [str]; +} + +final http = Http(); + +/* SNIPPET START */ +class AsyncTodosNotifier extends StateNotifier>> { + AsyncTodosNotifier() : super(const AsyncLoading()) { + _postInit(); + } + + Future _postInit() async { + state = await AsyncValue.guard(() async { + final json = await http.get('api/todos'); + + return [...json.map(Todo.fromJson)]; + }); + } + + // ... +} diff --git a/website/docs/migration/from_state_notifier/build_init/build_init.dart b/website/docs/migration/from_state_notifier/build_init/build_init.dart new file mode 100644 index 000000000..3e2303b7a --- /dev/null +++ b/website/docs/migration/from_state_notifier/build_init/build_init.dart @@ -0,0 +1,15 @@ +// ignore_for_file: avoid_print + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'build_init.g.dart'; + +/* SNIPPET START */ +@riverpod +class CounterNotifier extends _$CounterNotifier { + @override + int build() => 0; + + void increment() => state++; + void decrement() => state++; +} diff --git a/website/docs/migration/from_state_notifier/build_init/build_init.g.dart b/website/docs/migration/from_state_notifier/build_init/build_init.g.dart new file mode 100644 index 000000000..6b4621ecb --- /dev/null +++ b/website/docs/migration/from_state_notifier/build_init/build_init.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'build_init.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$counterNotifierHash() => r'8d4e4011da15a0ef79af9622336839a0c9e406ab'; + +/// See also [CounterNotifier]. +@ProviderFor(CounterNotifier) +final counterNotifierProvider = + AutoDisposeNotifierProvider.internal( + CounterNotifier.new, + name: r'counterNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$counterNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$CounterNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_state_notifier/build_init/index.tsx b/website/docs/migration/from_state_notifier/build_init/index.tsx new file mode 100644 index 000000000..276a143ac --- /dev/null +++ b/website/docs/migration/from_state_notifier/build_init/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./build_init.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_state_notifier/build_init/raw.dart b/website/docs/migration/from_state_notifier/build_init/raw.dart new file mode 100644 index 000000000..0ba8eebed --- /dev/null +++ b/website/docs/migration/from_state_notifier/build_init/raw.dart @@ -0,0 +1,15 @@ +// ignore_for_file: avoid_print + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +/* SNIPPET START */ +class CounterNotifier extends Notifier { + @override + int build() => 0; + + void increment() => state++; + void decrement() => state++; +} + +final counterNotifierProvider = NotifierProvider(CounterNotifier.new); diff --git a/website/docs/migration/from_state_notifier/build_init_old.dart b/website/docs/migration/from_state_notifier/build_init_old.dart new file mode 100644 index 000000000..82a8c54bd --- /dev/null +++ b/website/docs/migration/from_state_notifier/build_init_old.dart @@ -0,0 +1,13 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +/* SNIPPET START */ +class CounterNotifier extends StateNotifier { + CounterNotifier() : super(0); + + void increment() => state++; + void decrement() => state++; +} + +final counterNotifierProvider = StateNotifierProvider((ref) { + return CounterNotifier(); +}); diff --git a/website/docs/migration/from_state_notifier/consumers_dont_change.dart b/website/docs/migration/from_state_notifier/consumers_dont_change.dart new file mode 100644 index 000000000..6a964a5a6 --- /dev/null +++ b/website/docs/migration/from_state_notifier/consumers_dont_change.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class CounterNotifier extends StateNotifier { + CounterNotifier() : super(0); + + void increment() => state++; + void decrement() => state++; +} + +final counterNotifierProvider = StateNotifierProvider((ref) { + return CounterNotifier(); +}); + +/* SNIPPET START */ +class SomeConsumer extends ConsumerWidget { + const SomeConsumer({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + /* highlight-start */ + final counter = ref.watch(counterNotifierProvider); + /* highlight-end */ + return Column( + children: [ + Text("You've counted up until $counter, good job!"), + TextButton( + /* highlight-start */ + onPressed: ref.read(counterNotifierProvider.notifier).increment, + /* highlight-end */ + child: const Text('Count even more!'), + ) + ], + ); + } +} diff --git a/website/docs/migration/from_state_notifier/family_and_dispose/family_and_dispose.dart b/website/docs/migration/from_state_notifier/family_and_dispose/family_and_dispose.dart new file mode 100644 index 000000000..9a695614c --- /dev/null +++ b/website/docs/migration/from_state_notifier/family_and_dispose/family_and_dispose.dart @@ -0,0 +1,24 @@ +// ignore_for_file: unnecessary_this + +import 'dart:math'; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../utils.dart'; + +part 'family_and_dispose.g.dart'; + +/* SNIPPET START */ +@riverpod +class BugsEncounteredNotifier extends _$BugsEncounteredNotifier { + @override + FutureOr build(String featureId) { + return 99; + } + + Future fix(int amount) async { + final old = await future; + final result = await ref.read(taskTrackerProvider).fix(id: this.featureId, fixed: amount); + state = AsyncData(max(old - result, 0)); + } +} diff --git a/website/docs/migration/from_state_notifier/family_and_dispose/family_and_dispose.g.dart b/website/docs/migration/from_state_notifier/family_and_dispose/family_and_dispose.g.dart new file mode 100644 index 000000000..62ae74a00 --- /dev/null +++ b/website/docs/migration/from_state_notifier/family_and_dispose/family_and_dispose.g.dart @@ -0,0 +1,184 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'family_and_dispose.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$bugsEncounteredNotifierHash() => + r'c76e924f84db91c57d226896b062d9f4e8ab79e5'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$BugsEncounteredNotifier + extends BuildlessAutoDisposeAsyncNotifier { + late final String featureId; + + FutureOr build( + String featureId, + ); +} + +/// See also [BugsEncounteredNotifier]. +@ProviderFor(BugsEncounteredNotifier) +const bugsEncounteredNotifierProvider = BugsEncounteredNotifierFamily(); + +/// See also [BugsEncounteredNotifier]. +class BugsEncounteredNotifierFamily extends Family> { + /// See also [BugsEncounteredNotifier]. + const BugsEncounteredNotifierFamily(); + + /// See also [BugsEncounteredNotifier]. + BugsEncounteredNotifierProvider call( + String featureId, + ) { + return BugsEncounteredNotifierProvider( + featureId, + ); + } + + @visibleForOverriding + @override + BugsEncounteredNotifierProvider getProviderOverride( + covariant BugsEncounteredNotifierProvider provider, + ) { + return call( + provider.featureId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'bugsEncounteredNotifierProvider'; +} + +/// See also [BugsEncounteredNotifier]. +class BugsEncounteredNotifierProvider + extends AutoDisposeAsyncNotifierProviderImpl { + /// See also [BugsEncounteredNotifier]. + BugsEncounteredNotifierProvider( + String featureId, + ) : this._internal( + () => BugsEncounteredNotifier()..featureId = featureId, + from: bugsEncounteredNotifierProvider, + name: r'bugsEncounteredNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$bugsEncounteredNotifierHash, + dependencies: BugsEncounteredNotifierFamily._dependencies, + allTransitiveDependencies: + BugsEncounteredNotifierFamily._allTransitiveDependencies, + featureId: featureId, + ); + + BugsEncounteredNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.featureId, + }) : super.internal(); + + final String featureId; + + @override + FutureOr runNotifierBuild( + covariant BugsEncounteredNotifier notifier, + ) { + return notifier.build( + featureId, + ); + } + + @override + Override overrideWith(BugsEncounteredNotifier Function() create) { + return ProviderOverride( + origin: this, + override: BugsEncounteredNotifierProvider._internal( + () => create()..featureId = featureId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + featureId: featureId, + ), + ); + } + + @override + (String,) get argument { + return (featureId,); + } + + @override + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _BugsEncounteredNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is BugsEncounteredNotifierProvider && + other.featureId == featureId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, featureId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin BugsEncounteredNotifierRef on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `featureId` of this provider. + String get featureId; +} + +class _BugsEncounteredNotifierProviderElement + extends AutoDisposeAsyncNotifierProviderElement with BugsEncounteredNotifierRef { + _BugsEncounteredNotifierProviderElement(super.provider); + + @override + String get featureId => (origin as BugsEncounteredNotifierProvider).featureId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_state_notifier/family_and_dispose/index.tsx b/website/docs/migration/from_state_notifier/family_and_dispose/index.tsx new file mode 100644 index 000000000..0780f2135 --- /dev/null +++ b/website/docs/migration/from_state_notifier/family_and_dispose/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./family_and_dispose.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_state_notifier/family_and_dispose/raw.dart b/website/docs/migration/from_state_notifier/family_and_dispose/raw.dart new file mode 100644 index 000000000..8dfce9447 --- /dev/null +++ b/website/docs/migration/from_state_notifier/family_and_dispose/raw.dart @@ -0,0 +1,27 @@ +// ignore_for_file: unnecessary_this + +import 'dart:math'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../utils.dart'; + +/* SNIPPET START */ +class BugsEncounteredNotifier extends AutoDisposeFamilyAsyncNotifier { + @override + FutureOr build(String featureId) { + return 99; + } + + Future fix(int amount) async { + final old = await future; + final result = await ref.read(taskTrackerProvider).fix(id: this.arg, fixed: amount); + state = AsyncData(max(old - result, 0)); + } +} + +final bugsEncounteredNotifierProvider = + AsyncNotifierProvider.family.autoDispose( + BugsEncounteredNotifier.new, +); diff --git a/website/docs/migration/from_state_notifier/family_and_dispose_old.dart b/website/docs/migration/from_state_notifier/family_and_dispose_old.dart new file mode 100644 index 000000000..28f93a65c --- /dev/null +++ b/website/docs/migration/from_state_notifier/family_and_dispose_old.dart @@ -0,0 +1,30 @@ +// ignore_for_file: unnecessary_this + +import 'dart:math'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../utils.dart'; + +/* SNIPPET START */ +class BugsEncounteredNotifier extends StateNotifier> { + BugsEncounteredNotifier({ + required this.ref, + required this.featureId, + }) : super(const AsyncData(99)); + final String featureId; + final Ref ref; + + Future fix(int amount) async { + state = await AsyncValue.guard(() async { + final old = state.requireValue; + final result = await ref.read(taskTrackerProvider).fix(id: featureId, fixed: amount); + return max(old - result, 0); + }); + } +} + +final bugsEncounteredNotifierProvider = + StateNotifierProvider.family.autoDispose((ref, id) { + return BugsEncounteredNotifier(ref: ref, featureId: id); +}); diff --git a/website/docs/migration/from_state_notifier/from_state_provider/from_state_provider.dart b/website/docs/migration/from_state_notifier/from_state_provider/from_state_provider.dart new file mode 100644 index 000000000..2c71d6144 --- /dev/null +++ b/website/docs/migration/from_state_notifier/from_state_provider/from_state_provider.dart @@ -0,0 +1,14 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'from_state_provider.g.dart'; + +/* SNIPPET START */ +@riverpod +class CounterNotifier extends _$CounterNotifier { + @override + int build() => 0; + + @override + set state(int newState) => super.state = newState; + int update(int Function(int state) cb) => state = cb(state); +} diff --git a/website/docs/migration/from_state_notifier/from_state_provider/from_state_provider.g.dart b/website/docs/migration/from_state_notifier/from_state_provider/from_state_provider.g.dart new file mode 100644 index 000000000..9a3edeebc --- /dev/null +++ b/website/docs/migration/from_state_notifier/from_state_provider/from_state_provider.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'from_state_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$counterNotifierHash() => r'b32033040f0fff627f1a6dfd9cfb4e93a842390b'; + +/// See also [CounterNotifier]. +@ProviderFor(CounterNotifier) +final counterNotifierProvider = + AutoDisposeNotifierProvider.internal( + CounterNotifier.new, + name: r'counterNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$counterNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$CounterNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_state_notifier/from_state_provider/index.tsx b/website/docs/migration/from_state_notifier/from_state_provider/index.tsx new file mode 100644 index 000000000..f59794999 --- /dev/null +++ b/website/docs/migration/from_state_notifier/from_state_provider/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./from_state_provider.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_state_notifier/from_state_provider/raw.dart b/website/docs/migration/from_state_notifier/from_state_provider/raw.dart new file mode 100644 index 000000000..97e4564f3 --- /dev/null +++ b/website/docs/migration/from_state_notifier/from_state_provider/raw.dart @@ -0,0 +1,13 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +/* SNIPPET START */ +class CounterNotifier extends Notifier { + @override + int build() => 0; + + @override + set state(int newState) => super.state = newState; + int update(int Function(int state) cb) => state = cb(state); +} + +final counterNotifierProvider = NotifierProvider(CounterNotifier.new); diff --git a/website/docs/migration/from_state_notifier/from_state_provider_old.dart b/website/docs/migration/from_state_notifier/from_state_provider_old.dart new file mode 100644 index 000000000..246f44a0c --- /dev/null +++ b/website/docs/migration/from_state_notifier/from_state_provider_old.dart @@ -0,0 +1,6 @@ +import 'package:riverpod/riverpod.dart'; + +/* SNIPPET START */ +final counterProvider = StateProvider((ref) { + return 0; +}); diff --git a/website/docs/migration/from_state_notifier/obtain_notifier_on_tests.dart b/website/docs/migration/from_state_notifier/obtain_notifier_on_tests.dart new file mode 100644 index 000000000..df97a0fb2 --- /dev/null +++ b/website/docs/migration/from_state_notifier/obtain_notifier_on_tests.dart @@ -0,0 +1,33 @@ +// ignore_for_file: unused_local_variable,omit_local_variable_types + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class MyNotifier extends AutoDisposeNotifier { + @override + int build() { + return 0; + } +} + +final myNotifierProvider = NotifierProvider.autoDispose(MyNotifier.new); + +/* SNIPPET START */ +void main(List args) { + test('my test', () { + final container = ProviderContainer(); + addTearDown(container.dispose); + + // Obtaining a notifier + /* highlight-start */ + final AutoDisposeNotifier notifier = container.read(myNotifierProvider.notifier); + /* highlight-end */ + + // Obtaining its exposed state + /* highlight-start */ + final int state = container.read(myNotifierProvider); + /* highlight-end */ + + // TODO write your tests + }); +} diff --git a/website/docs/migration/from_state_notifier/old_lifecycles/index.tsx b/website/docs/migration/from_state_notifier/old_lifecycles/index.tsx new file mode 100644 index 000000000..9b77f551a --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./old_lifecycles.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_state_notifier/old_lifecycles/old_lifecycles.dart b/website/docs/migration/from_state_notifier/old_lifecycles/old_lifecycles.dart new file mode 100644 index 000000000..a96f294c6 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles/old_lifecycles.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../utils.dart'; + +part 'old_lifecycles.g.dart'; + +final repositoryProvider = Provider<_MyRepo>((ref) { + return _MyRepo(); +}); + +class _MyRepo { + Future update(int i, {CancelToken? token}) async {} +} + +/* SNIPPET START */ +@riverpod +class MyNotifier extends _$MyNotifier { + @override + int build() { + // Just read/write the code here, in one place + final period = ref.watch(durationProvider); + final timer = Timer.periodic(period, (t) => update()); + ref.onDispose(timer.cancel); + + return 0; + } + + Future update() async { + await ref.read(repositoryProvider).update(state + 1); + // `mounted` is no more! + state++; // This might throw. + } +} diff --git a/website/docs/migration/from_state_notifier/old_lifecycles/old_lifecycles.g.dart b/website/docs/migration/from_state_notifier/old_lifecycles/old_lifecycles.g.dart new file mode 100644 index 000000000..d28e242a3 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles/old_lifecycles.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'old_lifecycles.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myNotifierHash() => r'0495c52ce893ee0304d4d5ac5648c634ed4a241e'; + +/// See also [MyNotifier]. +@ProviderFor(MyNotifier) +final myNotifierProvider = + AutoDisposeNotifierProvider.internal( + MyNotifier.new, + name: r'myNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$myNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MyNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_state_notifier/old_lifecycles/raw.dart b/website/docs/migration/from_state_notifier/old_lifecycles/raw.dart new file mode 100644 index 000000000..691d71af9 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles/raw.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../utils.dart'; + +final repositoryProvider = Provider<_MyRepo>((ref) { + return _MyRepo(); +}); + +class _MyRepo { + Future update(int i, {CancelToken? token}) async {} +} + +/* SNIPPET START */ +class MyNotifier extends Notifier { + @override + int build() { + // Just read/write the code here, in one place + final period = ref.watch(durationProvider); + final timer = Timer.periodic(period, (t) => update()); + ref.onDispose(timer.cancel); + + return 0; + } + + Future update() async { + await ref.read(repositoryProvider).update(state + 1); + // `mounted` is no more! + state++; // This might throw. + } +} + +final myNotifierProvider = NotifierProvider(MyNotifier.new); diff --git a/website/docs/migration/from_state_notifier/old_lifecycles_final/index.tsx b/website/docs/migration/from_state_notifier/old_lifecycles_final/index.tsx new file mode 100644 index 000000000..9823b1564 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles_final/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./old_lifecycles_final.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_state_notifier/old_lifecycles_final/old_lifecycles_final.dart b/website/docs/migration/from_state_notifier/old_lifecycles_final/old_lifecycles_final.dart new file mode 100644 index 000000000..47bfe2516 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles_final/old_lifecycles_final.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../utils.dart'; + +part 'old_lifecycles_final.g.dart'; + +final repositoryProvider = Provider<_MyRepo>((ref) { + return _MyRepo(); +}); + +class _MyRepo { + Future update(int i, {CancelToken? token}) async {} +} + +/* SNIPPET START */ +@riverpod +class MyNotifier extends _$MyNotifier { + @override + int build() { + // Just read/write the code here, in one place + final period = ref.watch(durationProvider); + final timer = Timer.periodic(period, (t) => update()); + ref.onDispose(timer.cancel); + + return 0; + } + + Future update() async { + final cancelToken = CancelToken(); + ref.onDispose(cancelToken.cancel); + await ref.read(repositoryProvider).update(state + 1, token: cancelToken); + // When `cancelToken.cancel` is invoked, a custom Exception is thrown + state++; + } +} diff --git a/website/docs/migration/from_state_notifier/old_lifecycles_final/old_lifecycles_final.g.dart b/website/docs/migration/from_state_notifier/old_lifecycles_final/old_lifecycles_final.g.dart new file mode 100644 index 000000000..7c55609e1 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles_final/old_lifecycles_final.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'old_lifecycles_final.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myNotifierHash() => r'8ea2586ea29d12306efd4b8b847142136dd20338'; + +/// See also [MyNotifier]. +@ProviderFor(MyNotifier) +final myNotifierProvider = + AutoDisposeNotifierProvider.internal( + MyNotifier.new, + name: r'myNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$myNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MyNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/website/docs/migration/from_state_notifier/old_lifecycles_final/raw.dart b/website/docs/migration/from_state_notifier/old_lifecycles_final/raw.dart new file mode 100644 index 000000000..94814a191 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles_final/raw.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../utils.dart'; + +final repositoryProvider = Provider<_MyRepo>((ref) { + return _MyRepo(); +}); + +class _MyRepo { + Future update(int i, {CancelToken? token}) async {} +} + +/* SNIPPET START */ +class MyNotifier extends Notifier { + @override + int build() { + // Just read/write the code here, in one place + final period = ref.watch(durationProvider); + final timer = Timer.periodic(period, (t) => update()); + ref.onDispose(timer.cancel); + + return 0; + } + + Future update() async { + final cancelToken = CancelToken(); + ref.onDispose(cancelToken.cancel); + await ref.read(repositoryProvider).update(state + 1, token: cancelToken); + state++; + } +} + +final myNotifierProvider = NotifierProvider(MyNotifier.new); diff --git a/website/docs/migration/from_state_notifier/old_lifecycles_old.dart b/website/docs/migration/from_state_notifier/old_lifecycles_old.dart new file mode 100644 index 000000000..8f6eaa485 --- /dev/null +++ b/website/docs/migration/from_state_notifier/old_lifecycles_old.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../utils.dart'; + +final repositoryProvider = Provider<_MyRepo>((ref) { + return _MyRepo(); +}); + +class _MyRepo { + Future update(int i, {CancelToken? token}) async {} +} + +/* SNIPPET START */ +class MyNotifier extends StateNotifier { + MyNotifier(this.ref, this.period) : super(0) { + // 1 init logic + _timer = Timer.periodic(period, (t) => update()); // 2 side effect on init + } + final Duration period; + final Ref ref; + late final Timer _timer; + + Future update() async { + await ref.read(repositoryProvider).update(state + 1); // 3 mutation + if (mounted) state++; // 4 check for mounted props + } + + @override + void dispose() { + _timer.cancel(); // 5 custom dispose logic + super.dispose(); + } +} + +final myNotifierProvider = StateNotifierProvider((ref) { + // 6 provider definition + final period = ref.watch(durationProvider); // 7 reactive dependency logic + return MyNotifier(ref, period); // 8 pipe down `ref` +}); diff --git a/website/docs/migration/utils.dart b/website/docs/migration/utils.dart new file mode 100644 index 000000000..e515b6f94 --- /dev/null +++ b/website/docs/migration/utils.dart @@ -0,0 +1,23 @@ +import 'dart:math' as math; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final randomProvider = Provider((ref) { + return math.Random().nextInt(6); +}); + +final taskTrackerProvider = Provider((ref) { + return TaskTrackerRepo(); +}); + +class TaskTrackerRepo { + Future fix({required String id, required int fixed}) async => 0; +} + +final durationProvider = Provider((ref) { + return Duration.zero; +}); + +final availableWaterProvider = Provider((ref) { + return 40; +}); diff --git a/website/sidebars.js b/website/sidebars.js index 8d31d85f5..1335c0298 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -164,12 +164,8 @@ module.exports = { label: "Migration guides", collapsible: false, items: [ - // { - // type: "link", - // label: "Migrating StateNotifier/ChangeNotifier to Notifier (WIP)", - // href: "https://github.com/rrousselGit/riverpod/tree/master/examples/marvel", - // }, - + "migration/from_state_notifier", + "migration/from_change_notifier", "migration/0.14.0_to_1.0.0", "migration/0.13.0_to_0.14.0", ], diff --git a/website/src/documents_meta.js b/website/src/documents_meta.js index 958d51c78..83edcd4a7 100644 --- a/website/src/documents_meta.js +++ b/website/src/documents_meta.js @@ -7,6 +7,8 @@ export const documentTitles = { "providers/notifier_provider": "(Async)NotifierProvider", "providers/future_provider": "FutureProvider", "providers/change_notifier_provider": "ChangeNotifierProvider", + "migration/from_state_notifier": "From `StateNotifier`", + "migration/from_change_notifier": "From `ChangeNotifier`", "migration/0.14.0_to_1.0.0": "^0.14.0 to ^1.0.0", "migration/0.13.0_to_0.14.0": "^0.13.0 to ^0.14.0", "introduction/why_riverpod": "Why Riverpod?",