执行副作用
到目前为止,我们只看到了如何获取数据(也就是执行 GET HTTP 请求)。
但是副作用(例如 POST 请求)呢?
应用程序通常实现 CRUD(创建、读取、更新、删除)API。
执行此操作时,更新请求(通常是 POST)通常还应更新本地缓存,以使 UI 反映新状态。
问题是,我们如何从消费者程序内部更新提供者程序的状态?
理所当然的,提供者程序不会公开修改其状态的方法。
这是设计使然,以确保仅以受控方式修改状态并促进关注点分离。
相反,提供者程序必须显式公开修改其状态的方法。
为此,我们将使用一个新概念:通知者程序(Notifiers)。
为了展示这个新概念,让我们使用一个更高级的例子:待办事项列表。
定义通知者程序
让我们从此时我们已经知道的内容开始:一个简单的 GET 请求。 正如之前在开始你的第一次 provider/network 请求中看到的那样, 我们可以通过编写以下内容来获取待办事项列表:
class Todo with _$Todo {
factory Todo({
required String description,
(false) bool completed,
}) = _Todo;
}
Future<List<Todo>> todoList(Ref ref) async {
// 模拟一个网络请求。这通常来自真实的 API
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}
现在我们已经获取了待办事项列表,让我们看看如何添加新的待办事项。
为此,我们需要修改我们的提供者程序,以便它们公开一个公共 API 来修改其状态。
这是通过将我们的提供者程序转换为我们所说的“通知者程序”来完成的。
通知者程序是提供者程序的“有状态小部件”。它们需要对定义提供者程序的语法稍作调整。
此新语法如下:
@riverpod class MyNotifier extends _$MyNotifier { @override Result build() { <你的业务逻辑在这里> } <你的方法在这里> }
注解 | 所有提供者程序都必须使用 例如,我们可以通过编写 |
通知者程序 | 当 通知者程序负责公开修改提供者程序状态的方法。 备注 除了内置的 |
build 方法 | 所有通知者程序都必须重写该 不应直接调用此方法。 |
作为参考,您可能需要查看开始你的第一次 provider/network 请求,将这里的新语法与之前看到的语法进行比较。
除了 build
以外,没有其他方法的通知者程序与使用前面看到的语法相同。
现在我们已经了解了语法,让我们看看如何将之前定义的提供者程序转换为通知者程序:
class TodoList extends _$TodoList {
Future<List<Todo>> build() async {
// 我们之前在 FutureProvider 中的业务逻辑现在位于 build 方法中。
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}
}
请注意,在小部件中读取提供者程序的方式保持不变。
您仍然可以像以前的语法一样使用 ref.watch(todoListProvider)
。
不要将逻辑放在通知者程序的构造函数中。
通知者程序不应具有构造函数,因为 ref
此时其他属性尚不可用。
相反,将您的逻辑放在方法中 build
。
class MyNotifier extends ... {
MyNotifier() {
// ❌ 别这样做
// 这将会抛出一个异常
state = AsyncValue.data(42);
}
Result build() {
// ✅ 应该这样做
state = AsyncValue.data(42);
}
}
公开用于执行 POST 请求的方法
现在我们有了通知者程序,我们可以开始添加方法来执行副作用。
其中一个副作用是让客户端 POST 一个新的待办事项。
我们可以通过在通知者程序上添加一个 addTodo
方法来做到这一点:
class TodoList extends _$TodoList {
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/network 请求中看到的相同 Consumer
/ConsumerWidget
调用此方法:
class Example extends ConsumerWidget {
const Example({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// Using "ref.read" combined with "myProvider.notifier", we can
// obtain the class instance of our notifier. This enables us
// to call the "addTodo" method.
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 不会更新以返回新的待办事项列表。
我们希望本地缓存与服务器的状态相匹配。
有几种方法可以做到这一点,下面说说优点和缺点。
更新本地缓存以匹配 API 响应
一种常见的后端做法是让 POST 请求返回资源的新状态。
特别是,我们的 API 将在添加新的待办事项后返回新的待办事项列表。
一种方法是编写 state = AsyncData(response)
:
Future<void> addTodo(Todo todo) async {
// The POST request will return a List<Todo> matching the new application state
final response = await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// We decode the API response and convert it to a List<Todo>
List<Todo> newTodos = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>()
.map(Todo.fromJson)
.toList();
// We update the local cache to match the new state.
// This will notify all listeners.
state = AsyncData(newTodos);
}
- UI 将尽可能具有最新状态。如果其他用户添加了待办事项,我们也会看到它。
- 服务器是事实的来源。使用这种方法,客户端不需要知道需要在列表的哪个位置插入新的待办事项。
- 只需要一个网络请求。
- 仅当服务器以特定方式实现时,此方法才有效。如果服务器不返回新状态,则此方法将不起作用。
- 如果关联的 GET 请求更复杂,例如如果它具有过滤/排序的功能,则可能仍然不可行。
使用 ref.invalidateSelf()
刷新提供者程序。
一种选择是让我们的提供者程序重新执行 GET 请求。
这可以通过在 POST 请求之后调用 ref.invalidateSelf()
来完成:
Future<void> addTodo(Todo todo) async {
// We don't care about the API response
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// Once the post request is done, we can mark the local cache as dirty.
// This will cause "build" on our notifier to asynchronously be called again,
// and will notify listeners when doing so.
ref.invalidateSelf();
// (Optional) We can then wait for the new state to be computed.
// This ensures "addTodo" does not complete until the new state is available.
await future;
}
- UI 将尽可能具有最新状态。如果其他用户添加了待办事项,我们也会看到它。
- 服务器是事实的来源。使用这种方法,客户端不需要知道需要在列表的哪个位置插入新的待办事项。
- 无论服务器实现如何,此方法都应该有效。如果您的 GET 请求更复杂,例如它具有过滤器/排序,则它可能特别有用。
- 此方法将执行额外的 GET 请求,这可能效率低下。
手动更新本地缓存
另一种选择是手动更新本地缓存。
这将涉及尝试模仿后端的行为。例如,我们需要知道后端是在开头还是结尾插入新项目。
Future<void> addTodo(Todo todo) async {
// We don't care about the API response
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// We can then manually update the local cache. For this, we'll need to
// obtain the previous state.
// Caution: The previous state may still be loading or in error state.
// A graceful way of handling this would be to read `this.future` instead
// of `this.state`, which would enable awaiting the loading state, and
// throw an error if the state is in error state.
final previousState = await future;
// We can then update the state, by creating a new state object.
// This will notify all listeners.
state = AsyncData([...previousState, todo]);
}
此示例使用不可变状态。这不是必需的,但建议这样做。
有关更多详细信息,请参阅Why Immutability。
如果要改用可变状态,也可以执行以下操作:
final previousState = await future;
// Mutable the previous list of todos.
previousState.add(todo);
// Manually notify listeners.
ref.notifyListeners();
- 无论服务器实现如何,此方法都应该有效。
- 只需要一个网络请求。
- 本地缓存可能与服务器的状态不匹配。如果其他用户添加了待办事项,我们将看不到它。
- 这种方法的实现和有效地复制后端的逻辑可能更复杂。
更进一步:显示下拉加载器和错误处理
到目前为止我们所看到的一切,我们有一个按钮,当按下时会发出 POST 请求;
请求完成后,UI 会更新以反映更改。
但目前,没有迹象表明请求正在执行,如果失败,也没有任何信息。
一种方法是将返回 addTodo
的异步结果存储在本地小部件状态中,
然后监听该异步状态以显示下拉加载器或错误消息。
这时flutter_hooks派上用场的一种情况。
但是,当然,您也可以使用 StatefulWidget
代替。
以下代码片段显示了当处于加载状态时,进度指示器和操作处于挂起状态。 如果失败,则将按钮呈现为红色:
class Example extends ConsumerStatefulWidget {
const Example({super.key});
ConsumerState<ConsumerStatefulWidget> createState() => _ExampleState();
}
class _ExampleState extends ConsumerState<Example> {
// 待处理的 addTodo 操作。如果没有待处理的,则为 null。
Future<void>? _pendingAddTodo;
Widget build(BuildContext context) {
return FutureBuilder(
// 我们监听待处理的操作,以相应地更新 UI。
future: _pendingAddTodo,
builder: (context, snapshot) {
// 计算是否存在错误状态。
// 检查 connectionState 用于在重试操作时进行处理。
final isErrored = snapshot.hasError &&
snapshot.connectionState != ConnectionState.waiting;
return Row(
children: [
ElevatedButton(
style: ButtonStyle(
// 如果出现错误,我们会将该按钮显示为红色
backgroundColor: WidgetStatePropertyAll(
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(),
]
],
);
},
);
}
}