从 `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();
});
该实现显示了几个薄弱的设计选择,例如:
- 使用
isLoading
和hasError
处理不同的异步情况 - 需要仔细处理带有繁琐的
try
/catch
/finally
表达式的请求 - 需要在正确的时间调用
notifyListeners
才能使此实现发挥作用 - 存在不一致或可能不需要的状态,例如使用空列表进行初始化
请注意这个示例是如何精心设计的,以向新手开发人员展示 ChangeNotifier
如何导致错误的设计方案;此外,另一个要点是可变状态可能比最初承诺的要困难得多。
Notifier
/AsyncNotifer
与不可变状态相结合,
可以带来更好的设计方案和更少的错误。
让我们看看如何将上述代码片段一步一步迁移到最新的 API。
开始迁移
首先,我们应该声明新的提供者程序/通知者程序:这需要一些思维过程,这取决于您独特的业务逻辑。
我们总结一下上面的要求:
- 状态用
List<Todo>
表示,通过网络调用获得,不带参数 - 状态还应该公开有关其
loading
、error
和data
状态的信息 - 状态可以通过一些公开的方法进行改变,因此一个函数是不够的
上述思考过程归结为回答以下问题:
- 是否需要一些副作用?
y
:使用 Riverpod 的基于类的 APIn
:使用 Riverpod 的基于函数的 API
- State 需要异步加载吗?
y
:让build
返回Future<T>
n
:让build
简单地返回T
- 是否需要一些参数?
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
没有丢失任何内容:
不需要初始化 isLoading
或 hasError
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);
}
}
语法和设计方案可能会有所不同,但最终我们只需要编写我们的请求并随后更新状态。 请参阅执行副作用。
迁移过程总结
让我们从操作的角度回顾一下上面应用的整个迁移过程。
- 我们已将初始化从构造函数中调用的自定义方法移至
build
- 我们删除了
todos
、isLoading
和hasError
属性:内部state
就足够了 - 我们已经删除了所有
try
-catch
-finally
块:返回 Future 就足够了 - 我们对副作用应用了相同的简化(
addTodo
) - 我们已经通过简单地重新分配
state
应用了突变