[译][官方文档] Flutter/Dart 状态管理库 RiverPod (七)- 概要 - 执行副作用

原文链接: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(),
            ]
          ],
        );
      },
    );
  }
}

相关推荐
早起的年轻人13 小时前
Flutter String 按 ,。分割
flutter
helloxmg1 天前
鸿蒙harmonyos next flutter通信之MethodChannel获取设备信息
flutter
helloxmg1 天前
鸿蒙harmonyos next flutter混合开发之开发package
flutter·华为·harmonyos
lqj_本人2 天前
flutter_鸿蒙next_Dart基础②List
flutter
lqj_本人2 天前
flutter_鸿蒙next_Dart基础①字符串
flutter
The_tuber_sadness2 天前
【Flutter】- 基础语法
flutter
helloxmg2 天前
鸿蒙harmonyos next flutter通信之BasicMessageChannel获取app版本号
flutter
linpengteng3 天前
使用 Flutter 开发数字钱包应用(Dompet App)
前端·flutter·firebase