原文链接:Performing side effects | Riverpod
pub:riverpod | Dart Package (flutter-io.cn)
译时版本: 2.4.5
之前翻译过 RiverPod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。
之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)
执行副作用
至今为止,只看了如何获取数据(也称作执行 GET HTTP 请求)。
但是副作用怎么办?比如 POST 请求?
应用通常会实现 CRUD(增、查、改、删)API。
对于这些常见的做法是更新请求(典型的是 POST)也会更新本地缓存使 UI 反映新的状态。
问题是,如何在消费者内部更新 provider 的状态?
provider 天然不会暴露修改其状态的方式。它就是这么设计的,要确保状态只会以受控的方式进行更新并促使关注点分离。
相反,provider 需要明确地暴露修改其状态的方式。
要做到这点,会使用一个新概念: Notifier 。
要展示这个新概念,需要用一个更高级的示例:一个 TODO 列表。
定义 Notifier
先以已知的技术点(一个简单的 GET 请求)开始。正如创建第一个 provider/网络 请求[中文]中看到的内容,可以编写代码获取 TODO 列表:
dart
@riverpod
Future<List<Todo>> todoList(TodoListRef ref) async {
// 模拟网络请求。正常来说是源于真实 API 的数据
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}
现在已经获取了 TODO 列表,现在看一下如何添加一个新的 TODO 。 为此需要修改 provider 这样它们可以暴露用于修改其状态的公开 API。可通过把 provider 转换成称作 "notifier" 的东西可以做到。
Notifier 是 provider 的"有状态组件"。定义 provider 需要略微修改下语法。
新的语法如下:
dart
@riverpod
class MyNotifier extends _$MyNotifier {
@override
Result build() {
<your logic here>
}
<your methods here>
}
| 注解 | 所有的 provider 都必须用 @riverpod 或 @Riverpod() 注解。该注解可放置在全局函数或类上。 通过该注释就能配置 provider 了。 例如,可以编写 @Riverpod(keepAlive: true) 来关闭 "自动清理" (后面会看到)。 |
| Notifier | @riverpod 注解放置在类上时,这个类就称作 "Notifier" 。 这个类必须继承 _$NotifierName ,它的 NotifierName 是类名。 该类负责暴露修改 provider 状态的方式。 可使用 ref.read(yourProvider.notifier).yourMethod() 的方式访问类的公开方法进行消费。 备注 Notifier 除了内置的 'state' 不应再有公开属性,因为那样 UI 就不会知道状态已经发生改变。 |
| build 方法 | 所有的 notifier 必须覆写 build 方法。 该方法和应该在非 notifier 的 provider 中放置业务逻辑的场所是等同的。 该方法不能直接调用。 |
作为参考,可能想确认下创建第一个 provider/网络 请求 [中文]来比较新语法和前面看到的语法。
信息
除了
build没有其它方法的 Notifier 和使用前面看到的语法是同样的。
创建第一个 provider/网络 请求[中文]中展示的语法可认为是不能从 UI 修改的简短版 notifier 。
现在已经看到了该语法,现在看一下如何将前面定义的 provider 转换成 notifier :
dart
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async {
// 之前 FutureProvider 里面的逻辑现在放到了 build 方法里。
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}
}
注意读取组件内部 provider 的方式没有变化。
仍然可以使用 ref.watch(todoListProvider) ,和之前的语法一样。
警告
不可将逻辑放在 notifier 的构造方法里。
Notifier 不应该有构造方法,这是因为
ref和其它属性在这时还不可用。所以需要把逻辑放置在build方法里。
dart
class MyNotifier extends ... {
MyNotifier() {
// ❌ 不可这样做
// 这会抛出一个异常This will throw an exception
state = AsyncValue.data(42);
}
@override
Result build() {
// ✅ 应该这样做
state = AsyncValue.data(42);
}
}
暴露方法执行 POST 请求
现在已经有了 Notifier ,可以开始添加方法以开启执行副作用了。 副作用之一会是客户端 POST 一个新的 TODO 。 可以向 notifier 中添加 addTodo 方法来实现。
dart
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async => [/* ... */];
Future<void> addTodo(Todo todo) async {
await http.post(
Uri.https('your_api.com', '/todos'),
// 序列化 Todo 对象并 POST 到服务器。
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
}
}
然后就可以在 UI 中使用在创建第一个 provider/网络 请求 [中文]中看到的 Consumer/ConsumerWidget 调用该方法。
dart
class Example extends ConsumerWidget {
const Example({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// 使用 "myProvider.notifier" 绑定的 "ref.read",
// 可以获取到 notifier 的类实例。
// 该实例可用来调用 "addTodo" 方法。
ref
.read(todoListProvider.notifier)
.addTodo(Todo(description: 'This is a new todo'));
},
child: const Text('Add todo'),
);
}
}
信息
注意,这里是用的
ref.read而不是ref.watch来调用方法。尽管
ref.watch从技术上也能用,但是在事件处理器(如 "onPressed" )里执行逻辑时建议使用ref.read。现在有按钮按下时,会发送 POST 请求。
尽管如此,这时 UI 并不会更新以反映新的 TODO 列表。我们会希望本地缓存能匹配服务器的状态。
有几种方式可以做到,不过各有优劣。
更新本地缓存匹配 API 响应
通常的后端实现是 POST 请求返回资源的新状态。
尤其是 API 会在添加新的 TODO 后返回新的 TODO 列表。做法之一是 state = AsyncData(response) :
dart
Future<void> addTodo(Todo todo) async {
// POST 请求会返回匹配新的应用状态的 List<Todo>
final response = await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// 解码 API 响应并转换成 List<Todo>
List<Todo> newTodos = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>()
.map(Todo.fromJson)
.toList();
// 更新本地缓存以匹配新状态
// 这会通知所有 listener (监听器)。
state = AsyncData(newTodos);
}
优点:
- UI 能得到最新的状态。如果其它用户添加了一个 TODO ,我们也能看到。
- 服务器是真实的资源。通过这种方式,客户端客户端无需知道把新的 TODO 插入到 TODO 列表的什么位置。
- 只需要单次网络请求。
缺点:
- 这种做法只适合服务端以特定的方式实现。如果服务端不返回新的状态,该做法则无法适用。
- 如果关联的 GET 请求很复杂该方法也不可行,如需要过滤/排序等。
使用 ref.invalidateSelf() 刷新 provider 。
一种选择是让 provider 重新执行 GET 请求。
这可通过在 POST 请求后调用 ref.invalidateSelf() 实现。
dart
Future<void> addTodo(Todo todo) async {
// 无需关心 API 响应
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// 一旦 POST 请求完成,可以将本地缓存标记为脏数据。
// 这会触发 "build" notifier 以再次异步调用,
// 同时也会通知 listener 。
ref.invalidateSelf();
// (可选)之后需要等待新的状态运算完。
// 这能确保 "addTodo" 到新状态可用才算完成。
await future;
}
优点:
- UI 能得到最新的状态。如果其它用户添加了一个 TODO ,我们也能看到。
- 客户端无需知道把新的 TODO 插入到 TODO 列表的什么位置。
- 该做法不会考虑服务器的实现。如果 GET 请求很复杂也非常有用,如需要过滤/排序等。
缺点:
- 这种做法需要多执行一次 GET 请求,并不高效。
手动更新本地缓存
另外一个选择是手动更新本地缓存。
这就牵扯尝试模仿后端的行为。举例来说,需要知道后端是把新项目插入到开始还是最后。
dart
Future<void> addTodo(Todo todo) async {
// 无需关心 API 响应
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// 之后可以手动更新本地缓存。
// 这就需要获取上一次的状态。
// 警告:上一次的状态有可能还在加载中或者是错误的状态。
// 一个优雅的处理方式是读取 `this.future` 而不是 `this.state` ,
// 这样就可以等待加载中状态,如果是错误状态的话,就抛出错误。
final previousState = await future;
// 之后创建新的状态对象更新状态。
// 这会通知所有的 listener 。
state = AsyncData([...previousState, todo]);
}
信息
该示例使用了不可变状态。这不是必须的,但是建议这样做。查看为什么要不可变了解更多细节。
如果想使用可变状态,可用以下替换方式:
dart
final previousState = await future;
// 改变上一次的 TODO 列表。
previousState.add(todo);
// 手动通知 listener 。
ref.notifyListeners();
优点:
- 该做法不会考虑服务端的实现。
- 只需要单次网络请求。
缺点:
- 本地缓存有可能和服务端状态不匹配。如果其它用户添加了 TODO ,我们不会看到。
- 要实现和高效复制后端的逻辑,该做法可能会很复杂。
进阶:展示 spinner 和错误处理
至今为止看到的所有内容,有一个按钮,当按下时发送 POST 请求;请求完成时,UI 更新以反映变化。
但是这时,请求执行时没有任何指示,失败也没有任何信息。
要对应该问题,需要在本地组件状态中存储 addTodo 返回的 Future ,然后监听该 future 以显示 snipper 或错误信息。
这是 flutter_hooks 派上用场的一个场景。
不过当然也可以使用 StatefulWidget 代替。
下面的代码片段展示了操作处理时的进度指示器。如果失败了,会将按钮渲染成红色:

dart
class Example extends ConsumerStatefulWidget {
const Example({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _ExampleState();
}
class _ExampleState extends ConsumerState<Example> {
// 进行中的 addTodo 操作。如果没有进行中的处理,则是 null 。
Future<void>? _pendingAddTodo;
@override
Widget build(BuildContext context) {
return FutureBuilder(
// 监听进行中的处理,相应地更新 UI 。
future: _pendingAddTodo,
builder: (context, snapshot) {
// 计算是否有错误状态。
// 把检查连接状态的处理放在这里是为了再次执行时处理。
final isErrored = snapshot.hasError &&
snapshot.connectionState != ConnectionState.waiting;
return Row(
children: [
ElevatedButton(
style: ButtonStyle(
// 如果有错误,则将按钮显示为红色
backgroundColor: MaterialStateProperty.all(
isErrored ? Colors.red : null,
),
),
onPressed: () {
// 用变量保持 addTodo 返回的 future 。
final future = ref
.read(todoListProvider.notifier)
.addTodo(Todo(description: 'This is a new todo'));
// 将 future 存储在本地状态中
setState(() {
_pendingAddTodo = future;
});
},
child: const Text('Add todo'),
),
// 操作正在进行中,显示一个进度指示器
if (snapshot.connectionState == ConnectionState.waiting) ...[
const SizedBox(width: 8),
const CircularProgressIndicator(),
]
],
);
},
);
}
}