跳到主要内容

从 `ChangeNotifier` 迁移

在 Riverpod 中,ChangeNotifierProvider 用于提供从 pkg:provider 的平滑过渡。

如果您刚刚开始迁移到 pkg:riverpod,请务必阅读专用指南 (请参阅快速开始)。 本文适用于已经过渡到 Riverpod,但想要彻底放弃 ChangeNotifier 的人们。

总而言之,从 ChangeNotifier 迁移到 AsyncNotifer 需要范式转换, 但它极大地简化了迁移后的代码。 另请参阅Why Immutability

以这个(错误的)例子为例:

class MyChangeNotifier extends ChangeNotifier {
MyChangeNotifier() {
_init();
}
List<Todo> todos = [];
bool isLoading = true;
bool hasError = false;

Future<void> _init() async {
try {
final json = await http.get('api/todos');
todos = [...json.map(Todo.fromJson)];
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}

Future<void> 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<MyChangeNotifier>((ref) {
return MyChangeNotifier();
});

该实现显示了几个薄弱的设计选择,例如:

  • 使用 isLoadinghasError 处理不同的异步情况
  • 需要仔细处理带有繁琐的 try/catch/finally 表达式的请求
  • 需要在正确的时间调用 notifyListeners 才能使此实现发挥作用
  • 存在不一致或可能不需要的状态,例如使用空列表进行初始化

请注意这个示例是如何精心设计的,以向新手开发人员展示 ChangeNotifier 如何导致错误的设计方案;此外,另一个要点是可变状态可能比最初承诺的要困难得多。

Notifier/AsyncNotifer 与不可变状态相结合, 可以带来更好的设计方案和更少的错误。

让我们看看如何将上述代码片段一步一步迁移到最新的 API。

开始迁移​

首先,我们应该声明新的提供者程序/通知者程序:这需要一些思维过程,这取决于您独特的业务逻辑。

我们总结一下上面的要求:

  • 状态用 List<Todo> 表示,通过网络调用获得,不带参数
  • 状态还应该公开有关其 loadingerrordata 状态的信息
  • 状态可以通过一些公开的方法进行改变,因此一个函数是不够的
提示

上述思考过程归结为回答以下问题:

  1. 是否需要一些副作用?
    • y:使用 Riverpod 的基于类的 API
    • n:使用 Riverpod 的基于函数的 API
  2. State 需要异步加载吗?
    • y:让 build 返回 Future<T>
    • n:让 build 简单地返回 T
  3. 是否需要一些参数?
    • y:让 build (或你的函数)接受它们
    • n:让 build (或你的函数)不接受额外的参数
信息

如果您使用的是 codegen,上述思考过程就足够了。
无需考虑正确的类名及其特定的 API。
@riverpod 仅要求您编写一个具有返回类型的类,然后就可以开始了。

从技术上讲,这里最合适的是定义一个 AutoDisposeAsyncNotifier<List<Todo>>, 它满足上述所有要求。让我们先写一些伪代码。


class MyNotifier extends _$MyNotifier {

FutureOr<List<Todo>> build() {
// TODO ...
return [];
}

Future<void> addTodo(Todo todo) async {
// TODO
}
}
提示

请记住:在 IDE 中使用代码片段可以获得一些指导,或者只是为了加快代码编写速度。 请参阅入门指南

考虑到 ChangeNotifier 的实现,我们不再需要声明 todos; 这样的变量是 state,它是用 build 隐式加载的。

事实上,Riverpod 的通知者程序一次可以暴露一个实体。

提示

Riverpod 的 API 是细粒度的;尽管如此,在迁移时, 您仍然可以定义自定义实体来保存多个值。首先考虑使用 Dart 3 的记录 来平滑迁移。

初始化​

初始化通知者程序很简单:只需在 build 内编写初始化逻辑即可。
我们现在可以摆脱旧的 _init 函数。


class MyNotifier extends _$MyNotifier {

FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
}

相对于旧的 _init ,新的 build 没有丢失任何内容: 不需要初始化 isLoadinghasError

Riverpod 将通过公开 AsyncValue<List<Todo>> 自动转换任何异步提供者程序, 并比两个简单的布尔标志更好地处理异步状态的复杂性。

事实上,任何 AsyncNotifier 都会有效地使编写额外的 try/catch/finally 成为处理异步状态的反模式。

突变和副作用​

就像初始化一样,执行副作用时,无需操作布尔标志, 例如 hasError,或编写额外的 try/catch/finally

下面,我们删除了所有样板文件并成功完全迁移了上面的示例:


class MyNotifier extends _$MyNotifier {

FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');

return [...json.map(Todo.fromJson)];
}

Future<void> addTodo(Todo todo) async {
// optional: state = const AsyncLoading();
final json = await http.post('api/todos');
final newTodos = [...json.map(Todo.fromJson)];
state = AsyncData(newTodos);
}
}
提示

语法和设计方案可能会有所不同,但最终我们只需要编写我们的请求并随后更新状态。 请参阅执行副作用

迁移过程总结

让我们从操作的角度回顾一下上面应用的整个迁移过程。

  1. 我们已将初始化从构造函数中调用的自定义方法移至 build
  2. 我们删除了 todosisLoadinghasError 属性:内部 state 就足够了
  3. 我们已经删除了所有 try-catch-finally 块:返回 Future 就足够了
  4. 我们对副作用应用了相同的简化(addTodo
  5. 我们已经通过简单地重新分配 state 应用了突变