一起养成写作习惯!这是我参与「掘金日新计划 · 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.watch, Provider
会自动知道结果必须重新计算的时机。
使用 Provider
减少 povider/组件的重新构建
Provider
的独特之处是即使 Provider
重新计算了(代表性的场合是使用 ref.watch 时),它也不会更新监听它的组件/provider,除非值发生了改变。
一个现实示例是启用/禁用分页视图的 previous/next 按钮:
在我们的示例中,我们会主要聚焦于 "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
就会重新构建。
这种变化既会改进按钮的性能,有趣的是也能受益于将逻辑提取到组件之外。