一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
Riverpod的官方文档有多国语言,但是没有汉语,所以个人简单翻译了一版。
官网文档:Riverpod
GitHub:GitHub - rrousselGit/river_pod
Pub:riverpod | Dart Package (flutter-io.cn)
译时版本:riverpod 1.0.3
读取 Provider
阅读该指南之前,确保首先 阅读关于 Provider 的内容 。
在该指南中,我们会看到如何消费 provider 。
获取 ref 对象
首先也是最重要的,在读取 provider 之前,需要获取 ref 对象。
这个对象允许我们和 provider 进行交互,可从组件或者其它 provider 。
从 provider 获取 ref
所有的 provider 都会接收 ref 作为参数:
final provider = Provider((ref) {
// 使用 ref 获取其它 povider
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
该参数可安全用于传递给 provider 暴露的值。
例如,一个常用的场景是传递 provider 的 ref 给 StateNotifier:
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});
class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);
final Ref ref;
void increment() {
// Counter 可能使用 `ref` 读取其它 provider
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
这样做就可以允许 Counter 类读取 provider 了。
从组件获取 ref
组件原本没有 ref 参数。但是 Riverpod 提供了多种方案用于从组件中获取 ref 参数。
继承 ConsumerWidget 代替 StatelessWidget
在组件树中获取 ref 的最常用的方式是用 ConsumerWidget 替换 StatelessWidget。
在使用上,ConsumerWidget 和 StatelessWidget 是相同的,仅有一点不同是它的构建方法上带有额外的参数: ref 对象。
一个典型的 ConsumerWidget 如下:
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// 使用 ref 监听 provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
继承 ConsumerStatefulWidget + ConsumerState 代替 StatefulWidget + State
类似于 ConsumerWidget, ConsumerStatefulWidget 和 ConsumerState 也等同于 StatefulWidget 带着它的 State,不同的是 state 带有 ref 对象。
这回,ref 不是作为构建方法的参数传递,但是它是 ConsumerState 对象的属性:
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" 可在 StatefulWidget 的所有生命周期中使用。
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
// 也可以在构建方法内部使用 "ref" 监听 provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
继承 HookConsumerWidget 代替 HookWidget
该选项用于 flutter_hooks 用户。由于 flutter_hooks 需要继承 HookWidget 来运转,所以使用 hook(钩子)的组件无法继承 ConsumerWidget。
hooks_riverpod 包暴露了一个新组件叫做 HookConsumerWidget。 HookConsumerWidget 同时有 ConsumerWidget 和 HookWidget的行为。它允许组件同时监听 provider 和使用 hook 。
示例可如下:
class HomeView extends HookConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget 允许在构建方法内部使用 hook
final state = useState(0);
// 我们可以使用 ref 参数监听 provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
继承 StatefulHookConsumerWidget 代替 HookWidget
该选项用于需要使用 StatefulWidget 生命周期方法作为 hook 的附加的 flutter_hooks用户。
示例可如下:
class HomeView extends StatefulHookConsumerWidget {
const HomeView({Key? key}) : super(key: key);
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" 可在 StatefulWidget 的所有生命周期中使用。
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
// 类似于 HookConsumerWidget,可以在 builder 内部使用 hook 。
final state = useState(0);
// 也可以在构建方法内部使用 "ref" 监听 provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
Consumer 和 HookConsumer 组件
最后一种在组件内部获取 ref 的方式是依靠 Consumer / HookConsumer。
这些组件类可用于在 builder 的回调中获取 ref,使用相同的属性作为 ConsumerWidget / HookConsumerWidget 。
照这样,这些组件就是获取 ref 的方式,而不需要定义一个类。
示例可如下:
Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// 类似于 HookConsumerWidget ,可以在 builder 内部使用 hook 。
final state = useState(0);
// 也可以使用 `ref` 参数监听 provider 。
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);
使用 ref 和 provider 交互
现在我们有了 ref ,可以开始使用它了。
ref 有三个主要的用法:
- 获取 provider 的值并监听改变,这样当值改变时,就会重新构建订阅该值的组件或 proivder 。使用
ref.watch可以实现这种方式。 - 在 provider 上添加一个监听器,用以执行如导航到新页面或 provider 改变的任何时刻来展示一个模态对话框之类的动作。使用
ref.listen可以实现这种方式。 - 忽略改动时获取 provider 的值。当我们需要在如点击之类的事件中的 provider 的值时,这种方式会有用。 使用
ref.read可以实现这种方式。
注意
无论何时,只要可用,推荐使用
ref.watch而非ref.read或ref.listen实现 Future。 借助于ref.watch,应用会同时可交互且是声明式,这使应用更可维护。
使用 ref.watch 观察 provider
ref.watch 在组件的 build 方法内部或 provider 的构造方法体中,可以使 组件/provider 监听 provider :
例如,provider 可使用 ref.watch 绑定多个 provider 到一个新值上。
一个示例是过滤 TODO 列表。我们可以有两个 provider :
filterTypeProvider,这个 provider 暴露了过滤器的当前类型(无、只表示已完成的任务、...)。todosProvider,这个 provider 暴露了整个任务列表。
并且使用 ref.watch ,我们可以创建第三个 provider ,用它绑定这两个 provider 来创建过滤后的任务列表:
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());
final filteredTodoListProvider = Provider((ref) {
// 同时获取过滤器和 TODO 列表
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// 返回完整的 TODO 列表
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// 返回未过滤的 TODO 列表
return todos;
}
});
用此代码,filteredTodoListProvider 现在暴露了过滤后的任务列表。
不管是过滤器还是任务列表发生改变,过滤后的列表都会自动更新;如果过滤器和任务列表都没有改变,过滤后的列表不会重新计算。
类似地,组件可以使用 ref.watch 来展示来自 provider 的内容,然后在内容改变的任何时刻更新用户接口:
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// 使用 ref 监听 provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
该片段展示了组件监听存储 count 的 provider 。并且如果 count 发生改变,组件会重新构建,UI 也会更新来展示新的值。
警告
watch方法不应该被异步调用,如在 ElevatedButton 的onPressed内部。也不应该在initState和其它的 State 生命周期中使用。在这些场景下,可以考虑使用
ref.read来代替。
使用 ref.listen 响应 provider 的改变
类似于 ref.watch ,可以使用 ref.listen 监听 provider 。
两者的主要区别是如果监听到 povider 的改变,相对于重新构建组件/provider ,使用 ref.listen 会替换为调用自定义函数。
当确定的改变发生时,该方法对于响应动作很有用。如当错误发生时,显示 snackbar 。
ref.listen 方法需要两个占位的参数,第一个是 Provider ,第二个是回调函数,该回调函数是当状态改变时要执行的函数。该回调函数被调用时,需要传递两个值,前一个状态的值和新状态的值。
ref.listen 方法可在 provider 的构造体内部使用:
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
或者在组件的 build 方法内部:
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
警告
listen方法不应该被异步调用,如在 ElevatedButton 的onPressed内部。也不应该在initState和其它的 State 生命周期中使用。
使用 ref.read 来获取 provider 的状态
ref.read 方法可无需监听 provider 就能获取 provider 的状态。
通常在用户交互触发的函数内部使用。例如,当用户点击按钮时,可以使用 ref.read 增加一个计数:
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// 在 `Counter` 上调用 `increment()`
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
注意事项
应该尽量避免使用
ref.read,因为它是非响应的。它只存在于使用
watch或listen会导致问题的场景。如果可以,更好的方式是使用watch/listen,特别是watch。
不能在 build 方法内部使用 ref.read
为了优化组件的性能,你可能会如下试图使用 ref.read :
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
// 使用 "read" 忽略 provider 的更新
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
但是,这是非常糟糕的实践,会引起难于追踪的 BUG 。
这种使用 ref.read 的方式通常是与『 provider 暴露的值从未发生改变,所以全用 ref.read 是安全的』的想法有关。
这种假设的问题是,当前 provider 可能从未更新它的值,并不能保证将来也是这样。
软件都趋向于有很多变化,更有可能的是在将来,以前从未有变化的值现在需要改变。
如果使用 ref.read ,当值需要改变时,就需要遍历整个代码库将 ref.read 改为 ref.watch - 因为这容易导致错误,你也可能会遗忘掉一些情况。
如果开始就使用 ref.watch ,重构时问题就会更少。
但我想使用 ref.read 来减少组件重构的次数
这个目标是值得赞扬的,重要的是可以使用 ref.watch 来代替来实现完全相同的效果(减少构建次数)。
Provider 提供了在减少重构次数时获取值的多种方式,可以使用它们来代替。
例如,代替
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
可以这样做:
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
这两个片段都能实现相同的效果:当增加计数时不会重新构建按钮。
另外,第二种方式支持重置计数器的情况。例如,应用的其它部分可能会调用:
ref.refresh(counterProvider);
这会重新创建 StateController 对象。
如果在这里使用 ref.read ,按钮仍然会使用上一个 StateController 实例,它已经被释放,无法再被继续使用。
但是使用 ref.watch 能正确地重新构建按钮以使用新的 StateController 。
确定读取内容
依赖于要监听的 provider , 可能会有多个可以监听的值。
作为示例,考虑下面的 StreamProvider:
final userProvider = StreamProvider<User>(...);
读取 userProvider 时,你可以:
-
通过监听
userProvider自身同步读取当前状态:Widget build(BuildContext context, WidgetRef ref) { AsyncValue<User> user = ref.watch(userProvider); return user.when( loading: () => const CircularProgressIndicator(), error: (error, stack) => const Text('Oops'), data: (user) => Text(user.name), ); } -
通过监听
userProvider.stream,获取关联的 Stream :Widget build(BuildContext context, WidgetRef ref) { Stream<User> user = ref.watch(userProvider.stream); } -
通过监听
userProvider.future,获取分解最新提交值的 Future :Widget build(BuildContext context, WidgetRef ref) { Future<User> user = ref.watch(userProvider.future); }
其它 provider 可能提供可选择的不同值。
更多信息,参考 API reference 里每个 provider 的文档。
使用 select 过滤重构
最后一个要提到的关于读取 provider 的特性是从 ref.watch 来减少组件/provider 重新构建次数的能力,或者 ref.listen 执行函数的频度。
意识到这些很重要,默认情况下,监听 provider 会监听整个对象状态。但是,组件/provider 可能只关心一些属性的改变而不是整个对象。
例如,provider 可能会暴露一个 User :
abstract class User {
String get name;
int get age;
}
但是组件可能只使用用户名:
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
如果天真地使用 ref.watch ,当 age 改变时,会重新构建重个组件。
解决方案是使用 select 明确地告诉 Riverpod 只想监听 User 的 name 属性。
更新后的代码如下:
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
使用 select ,能够指定函数返回我们关心的属性。
无论何时 User 发生改变,Riverpod 都会调用该函数并比较之前的值和新的结果。如果它们不同(例如 name 发生了改变),Riverpod 就会重新构建组件。
不过,如果它们相等(例如 age 发生了改变),Riverpod 不会重新构建组件。
信息
可以在
ref.listen中使用select:ref.listen<String>( userProvider.select((user) => user.name), (String? previousName, String newName) { print('The user name changed $newName'); } );
这样做会只在 name 改变时调用 listener (监听器)。
提示
不需要返回对象的属性。所有覆写 == 的值都会正常运转。例如,可以:
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));