From 98cca355a08f13505cc4818d983e911f17f5f01a Mon Sep 17 00:00:00 2001 From: "venir.dev" Date: Thu, 24 Aug 2023 11:56:08 +0200 Subject: [PATCH] Added a migration example for `ChangeNotifier` --- .../from_change_notifier.dart | 28 ++++++ .../from_change_notifier.g.dart | 27 ++++++ .../migration/from_change_notifier/index.tsx | 9 ++ .../migration/from_change_notifier/raw.dart | 28 ++++++ .../docs/migration/from_state_notifier.mdx | 90 ++++++++++++++++++- 5 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 website/docs/migration/from_change_notifier/from_change_notifier.dart create mode 100644 website/docs/migration/from_change_notifier/from_change_notifier.g.dart create mode 100644 website/docs/migration/from_change_notifier/index.tsx create mode 100644 website/docs/migration/from_change_notifier/raw.dart diff --git a/website/docs/migration/from_change_notifier/from_change_notifier.dart b/website/docs/migration/from_change_notifier/from_change_notifier.dart new file mode 100644 index 000000000..490c0d268 --- /dev/null +++ b/website/docs/migration/from_change_notifier/from_change_notifier.dart @@ -0,0 +1,28 @@ +// ignore_for_file: avoid_print + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'from_change_notifier.g.dart'; + +class Todo { + const Todo(this.id); + final int id; +} + +/* SNIPPET START */ +@riverpod +class MyNotifier extends _$MyNotifier { + @override + FutureOr> build() { + // request mock + return Future.delayed(const Duration(seconds: 1), () => []); + } + + Future addTodo(int id) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() { + // request mock + return Future.delayed(const Duration(seconds: 1), () => [Todo(id)]); + }); + } +} diff --git a/website/docs/migration/from_change_notifier/from_change_notifier.g.dart b/website/docs/migration/from_change_notifier/from_change_notifier.g.dart new file mode 100644 index 000000000..7d8bc2da5 --- /dev/null +++ b/website/docs/migration/from_change_notifier/from_change_notifier.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'from_change_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myNotifierHash() => r'fa0927a77e9a6161041c5c38e9fada566bebe6bc'; + +/// 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 diff --git a/website/docs/migration/from_change_notifier/index.tsx b/website/docs/migration/from_change_notifier/index.tsx new file mode 100644 index 000000000..cff9637c7 --- /dev/null +++ b/website/docs/migration/from_change_notifier/index.tsx @@ -0,0 +1,9 @@ +import raw from "!!raw-loader!./raw.dart"; +import codegen from "!!raw-loader!./from_change_notifier.dart"; + +export default { + raw, + hooks: raw, + codegen, + hooksCodegen: codegen, +}; diff --git a/website/docs/migration/from_change_notifier/raw.dart b/website/docs/migration/from_change_notifier/raw.dart new file mode 100644 index 000000000..a2cbb31d4 --- /dev/null +++ b/website/docs/migration/from_change_notifier/raw.dart @@ -0,0 +1,28 @@ +// ignore_for_file: avoid_print + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +class Todo { + const Todo(this.id); + final int id; +} + +/* SNIPPET START */ +@riverpod +class MyNotifier extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() { + // request mock + return Future.delayed(const Duration(seconds: 1), () => []); + } + + Future addTodo(int id) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() { + // request mock + return Future.delayed(const Duration(seconds: 1), () => [Todo(id)]); + }); + } +} + +final myNotifierProvider = AsyncNotifierProvider.autoDispose(MyNotifier.new); diff --git a/website/docs/migration/from_state_notifier.mdx b/website/docs/migration/from_state_notifier.mdx index bba79799d..155876fb3 100644 --- a/website/docs/migration/from_state_notifier.mdx +++ b/website/docs/migration/from_state_notifier.mdx @@ -6,6 +6,7 @@ import buildIinit from "./build_init"; import familyAndDispose from "./family_and_dispose"; import asyncNotifier from "./async_notifier"; import addListener from "./add_listener"; +import fromChangeNotifier from "./from_change_notifier"; import fromStateProvider from "./from_state_provider"; import oldLifecycles from "./old_lifecycles"; import { AutoSnippet } from "../../src/components/CodeSnippet"; @@ -295,10 +296,91 @@ Becomes: Even though it costs us a few more LoC, migrating away from `StateProvider` enables us to definetively archive `StateNotifier`. -## From `ChangeNotifier` to `Notifier` +## From `ChangeNotifier` to `AsyncNotifier` -WIP +`ChangeNotifier` is meant to be used to offer a smooth transitionfrom pkg:Provider. +This paragraphs showcases a migration towards the new APIs going further than what's showcased +in the [quickstart guide](/docs/from_provider/quickstart). -### An asynchronous example +All in all, migrating from `ChangeNotifier` to `AsyncNotifer` requires a paradigm shift, but it +brings great simplification with the resulting migrated code. -WIP +Take this example: +```dart +class MyChangeNotifier extends ChangeNotifier { + MyChangeNotifier() { + _init(); + } + List todos = []; + bool isLoading = true; + bool hasError = false; + + Future _init() async { + try { + // request mock + todos = Future.delayed(const Duration(seconds: 1), () => []); + } on Exception { + hasError = true; + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future addTodo(int id) async { + isLoading = true; + notifyListeners(); + + try { + // request mock + todos = Future.delayed(const Duration(seconds: 1), () => [Todo(id)]); + hasError = false; + } on Exception { + hasError = true; + } finally { + isLoading = false; + notifyListeners(); + } + } +} + +final myChangeProvider = ChangeNotifierProvider((ref) { + return MyChangeNotifier(); +}); +``` + +This implementation shows several faulty 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 + +While this example has been crafted to show how `ChangeNotifier` can lead to faulty design choices +for newbie developers, the main takeaway is that mutable state might be harder than it looks. + +`Notifier`/`AsyncNotifer`, in combination with immutable state, can lead to better design choices +and less errors; this approach lifts the developer from several intricacies due to the nature +of asynchronous state. + +With `AsyncNotifier`, the above becomes: + + +### Migration Process +Let's analyze the migration process applied here: +1. We've moved the initialization from a method invoked in a constructor, directly into `build` +2. We've removed `todos`, `isLoading` and `hasError`: `state` suffice +3. Notice how, since `build` returns a `Future`, state is implicitly initialized with `AsyncLoading`, +waiting for the initial request to finish +4. We've returned the result of the (mocked) asynchronous request, with no try-catch-finally blocks +5. We've then simplified `addTodo`, exploiting `AsyncValue`'s APIs +6. Notice that, to apply mutations, we simply reassign `state` directly: +listeners will be automatically notified about it + +### Advantages +Finally, let's highlight the main advantages of the new APIs: +- There's a lot less code, and what's left is way more simple and readable +- Since `AsyncNotifier` implicitly uses `AsyncValue`, there's no need to manually define and handle + `isLoading` and `hasError` +- There's no need to explicitly handle errors (i.e. try-catch-finally blocks), since +`AsyncNotifier.build` converts errors into `AsyncError` and valid data into `AsyncData` +- `AsyncValue.guard` essentialy emulates `AsyncNotifier.build`, greatly simplifying mutations \ No newline at end of file