[官文翻译]Flutter状态管理库Riverpod - 所有的Provider - Provider

437 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情


Riverpod的官方文档有多国语言,但是没有汉语,所以个人简单翻译了一版。

官网文档:Riverpod

GitHub:GitHub - rrousselGit/river_pod

Pub:riverpod | Dart Package (flutter-io.cn)

译时版本:riverpod 1.0.3


Provider

Provider 是所有 Provider 中最基础的。它创建一些值。。,然后下面都是关于它的内容。

Provider 通常用于:

  • 缓存计算。
  • 对其它 provider 暴露值(如 Repository/HttpClient)。
  • 为测试或组件提供覆写值的途径。
  • 无需使用 select 减少 provider /组件的重新构建。

使用 Provider 缓存计算

使用 ref.watch 绑定时,Provider 是一个强大的用于缓存同步操作的工具。

一个示例是过滤 TODO 列表。因为过滤列表的成本稍微有些高,我们并不想应用在重新渲染时随时过滤 TODO 列表。这种情况下,会使用 Provider 为我们进行过滤。

对于该点,假定应用有现有的 StateNotifierProvider ,它控制TODO 列表:

class Todo {
  Todo(this.description, this.isCompleted);
  final bool isCompleted;
  final String description;
}

class TodosNotifier extends StateNotifier<List<Todo>> {
  TodosNotifier() : super([]);

  void addTodo(Todo todo) {
    state = [...state, todo];
  }
  // TODO 添加其它方法,如 "removeTodo" , ...
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});

从这里,我们这里使用 Provider 暴露过滤后的 TODO 列表,只显示完成的 TODO :

final completedTodosProvider = Provider<List<Todo>>((ref) {
  // 我们从 todosProvider 获取所有 TODO 的列表
  final todos = ref.watch(todosProvider);

  // 我们只返回完成的 TODO
  return todos.where((todo) => todo.isCompleted).toList();
});

使用这段代码,UI 现在可以通过监听 completedTodosProvider 显示完成的 TODO 列表:

Consumer(builder: (context, ref, child) {
  final completedTodos = ref.watch(completedTodosProvider);
  // TODO 使用 ListView/GridView/.. 显示 TODO
});

有趣的是,列表的过滤现在已经被缓存。

这意味着直到 TODO 被添加/移除/更新,完成的 TODO 列表都不会被重新计算,即使我们多次渲染完成的 TODO 列表。

注意当 TODO 列表发生改变时,我们是如何无需手动使缓存无效。 归功于 ref.watchProvider 会自动知道结果必须重新计算的时机。

使用 Provider 减少 povider/组件的重新构建

Provider 的独特之处是即使 Provider 重新计算了(代表性的场合是使用 ref.watch 时),它也不会更新监听它的组件/provider,除非值发生了改变。

一个现实示例是启用/禁用分页视图的 previous/next 按钮:

image.png

在我们的示例中,我们会主要聚焦于 "previous" 按钮。 这种按钮的单纯实现会是获取当前页码的组件,如果页码是 0 ,会禁用该按钮。

代码会是:

final pageIndexProvider = StateProvider<int>((ref) => 0);

class PreviousButton extends ConsumerWidget {
  const PreviousButton({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 如果不是第一页,previous 按钮是可用的
    final canGoToPreviousPage = ref.watch(pageIndexProvider) == 0;

    void goToPreviousPage() {
      ref.read(pageIndexProvider.notifier).update((state) => state - 1);
    }

    return ElevatedButton(
      onPressed: canGoToPreviousPage ? null : goToPreviousPage,
      child: const Text('previous'),
    );
  }
}

该代码的问题是无论何时我们改变当前页, "previous" 都会重新构建。 理想的情况下,我们只想在可用不可用之前变化时重新构建按钮。

该问题的根源是无论用户是否允许直接使用 "previous" 按钮进入上一页都会重新计算。

解决该问题的方法是从组件中提取出该逻辑到 Provider 中:

final pageIndexProvider = StateProvider<int>((ref) => 0);

// 不管用户是否允许进入上一页, provider 都会计算。
final canGoToPreviousPageProvider = Provider<bool>((ref) {
  return ref.watch(pageIndexProvider) == 0;
});

class PreviousButton extends ConsumerWidget {
  const PreviousButton({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 现在我们会监视新的 Provider
    // 不管能否进入上一页,我们的组件都不会再计算
    final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

    void goToPreviousPage() {
      ref.read(pageIndexProvider.notifier).update((state) => state - 1);
    }

    return ElevatedButton(
      onPressed: canGoToPreviousPage ? null : goToPreviousPage,
      child: const Text('previous'),
    );
  }
}

通过这样小的重构,由于 Provider 的原因,页码改变时 PreviousButton 组件不会再被重构。

现在开始页码改变时, canGoToPreviousPageProvider provider 会重新计算。但是如果 provider 暴露的值发生了改变, PreviousButton 就会重新构建。

这种变化既会改进按钮的性能,有趣的是也能受益于将逻辑提取到组件之外。