Skip to content

Commit

Permalink
Added a migration example for ChangeNotifier
Browse files Browse the repository at this point in the history
  • Loading branch information
lucavenir committed Aug 24, 2023
1 parent 09b16e5 commit 98cca35
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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<List<Todo>> build() {
// request mock
return Future.delayed(const Duration(seconds: 1), () => <Todo>[]);
}

Future<void> addTodo(int id) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() {
// request mock
return Future.delayed(const Duration(seconds: 1), () => [Todo(id)]);
});
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions website/docs/migration/from_change_notifier/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
};
28 changes: 28 additions & 0 deletions website/docs/migration/from_change_notifier/raw.dart
Original file line number Diff line number Diff line change
@@ -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<List<Todo>> {
@override
FutureOr<List<Todo>> build() {
// request mock
return Future.delayed(const Duration(seconds: 1), () => <Todo>[]);
}

Future<void> 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, int>(MyNotifier.new);
90 changes: 86 additions & 4 deletions website/docs/migration/from_state_notifier.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Todo> todos = [];
bool isLoading = true;
bool hasError = false;
Future<void> _init() async {
try {
// request mock
todos = Future.delayed(const Duration(seconds: 1), () => <Todo>[]);
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> 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<MyChangeNotifier>((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:
<AutoSnippet language="dart" {...fromChangeNotifier}></AutoSnippet>

### 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<T>`, 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

0 comments on commit 98cca35

Please sign in to comment.