读取 Provider
在阅读本指南之前,请确保先阅读有关Providers的内容。
在本指南中,我们将了解如何使用provider。
获取一个“ref”对象
首先,也是最重要的,在读取provider之前,我们需要获取一个“ref”对象。
这个对象能够让我们与provider交互,不管是来自widget还是其他provider。
从provider获取“ref”
所有provider都接收一个“ref”作为参数:
String value(ValueRef ref) {
// 使用ref获取其他provider
final repository = ref.watch(repositoryProvider);
return repository.get();
}
将此参数传递给provider暴露的值是安全的。
class Counter extends _$Counter {
int build() => 0;
void increment() {
// Counter可以使用“ref”读取其他provider
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
这样做允许Counter
类读取provider。
从widget获取“ref”
Widget自然没有ref参数。但是 Riverpod 提供了多种从widget中获取ref的解决方案。
扩展ConsumerWidget而不是StatelessWidget
在widget树中获取ref的最常用方法是将 StatelessWidget 替换为 ConsumerWidget 。
ConsumerWidget 在用法上与 StatelessWidget 相同,唯一的区别是它在构建方法上有一个额外的参数:“ref”对象。
一个典型的 ConsumerWidget 如下所示:
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// 使用ref监听provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
扩展ConsumerStatefulWidget+ConsumerState而不是StatefulWidget+State
与 ConsumerWidget 类似, ConsumerStatefulWidget 和 ConsumerState 等价于带有状态的StatefulWidget,区别在于状态中有一个“ref”对象。
这一次,“ref”没有作为构建方法的参数传递,而是作为 ConsumerState 对象的属性传递:
class HomeView extends ConsumerStatefulWidget {
const HomeView({super.key});
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
void initState() {
super.initState();
// “ref” 可以在 StatefulWidget 的所有的生命周期内使用。
ref.read(counterProvider);
}
Widget build(BuildContext context) {
// 我们也可以在build函数中使用“ref”监听provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
使用ref与provider交互
现在我们有了一个“ref”,我们可以开始使用它了。
“ref”有三种主要用法:
- 获取provider的值并监听更改,这样当该值发生更改时,
将重新构建订阅该值的widget或provider。
这是使用ref.watch
完成的 - 在provider上添加监听器,以执行诸如导航到新页面或每当provider更改时显示模态框等操作。
这是使用ref.listen
完成的。 - 在忽略更改的情况下获取provider的值。
当我们在诸如“on click”之类的事件中需要provider的值时很有用。
这是使用ref.read
完成的。
尽可能使用 ref.watch
而不是 ref.read
或 ref.listen
来实现你的功能。
通过依赖 ref.watch
,你的应用变得既具有响应性又具有声明性,这使得项目会更易于维护。
Using ref.watch to observe a provider
使用 ref.watch 观察provider
Ref.watch在widget的build
方法中或在provider的主体中使用,以使widget/provider监听provider:
例如,一个provider可以使用 ref.watch
将多个provider组合成一个新值。
筛选待办清单就是一个例子。我们可以有两个provider:
filterTypeProvider
,一个能够暴露当前过滤器类型(不显示,只显示完成的内容等等)的provider。todosProvider
,暴露整个待办清单列表的provider。
通过 ref.watch
,我们可以创建第三个provider,
它结合了这两个provider来创建一个过滤过的待办清单列表:
FilterType filterType(FilterTypeRef ref) {
return FilterType.none;
}
class Todos extends _$Todos {
List<Todo> build() {
return [];
}
}
List<Todo> filteredTodoList(FilteredTodoListRef ref) {
// 获取筛选器和待办清单列表
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// 返回完成的待办清单
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// 返回所有的待办清单
return todos;
}
}
有了这段代码,filteredTodoListProvider
现在暴露了过滤后的清单列表。
如果筛选器或待办清单列表发生变化,筛选后的列表也会自动更新。 同时,如果过滤器和待办清单列表都没有改变,则不会重新计算那个列表。
类似地,widget可以使用ref.watch显示来自provider的内容, 并在内容发生变化时更新用户界面:
int counter(CounterRef ref) => 0;
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// 使用ref监听provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
这个代码段显示了一个widget,它监听了存储count
的provider。
如果该count
发生变化,widget将重新构建,UI将更新以显示新的值。
像在 ElevatedButton 的 onPressed
中那样,watch
方法不应该被异步调用。
它也不应该在 initState
和其他 State 的生命周期中使用。
在这种情况下,请考虑使用 ref.read
。
使用ref.listen来响应provider的变化
与 ref.watch
类似,也可以使用ref.listen来观察一个provider。
The main difference between them is that, rather than rebuilding the widget/provider
if the listened to provider changes, using ref.listen
will instead call a custom function.
它们之间的主要区别是,如果监听的provider发生更改,
使用 ref.listen
将调用自定义的函数,而不是重新构建widget/provider。
这对于在发生特定变化时执行操作很有用,例如在发生错误时显示snackbar。
ref.listen
方法需要两个位置参数,第一个是Provider,第二个是当状态改变时我们想要执行的回调函数。
当调用回调函数时将传递前一个状态的值和新状态的值。
ref.listen
方法可以在provider内部使用:
void another(AnotherRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
}
或者在widget的 build
方法中:
class Counter extends _$Counter {
int build() => 0;
}
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
像在 ElevatedButton 的 onPressed
中那样,listen
方法不应该被异步调用。
它也不应该在 initState
和其他 State 的生命周期中使用。
使用ref.read获取一个provider的状态
ref.read
是一种不监听provider状态的方法。
它通常在用户交互触发的函数中使用。
例如,我们可以使用 ref.read
在用户单击按钮时将计数器数值加1:
class Counter extends _$Counter {
int build() => 0;
void increment() => state = state + 1;
}
class HomeView extends ConsumerWidget {
const HomeView({super.key});
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
来优化widget的性能:
class Counter extends _$Counter {
int build() => 0;
void increment() => state = state + 1;
}
Widget build(BuildContext context, WidgetRef ref) {
// 使用 “read” 忽略provider的更新
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: counter.increment,
child: const Text('button'),
);
}
但这样做是非常糟糕的,它可能会导致预料之外的bug。
以这种方式使用 ref.read
通常会让人觉得“provider暴露的值永远不会改变,
所以使用 ref.read
是安全的”。但问题是,
虽然现在的provider可能确实永远不会更新它的值,但你无法保证以后的值还是一样的。
应用往往会发生很多变更,假设在未来,以前从未改变的一个值将需要改变。
如果你使用 ref.read
,当该值需要更改时,你必须遍历整个代码库将 ref.read
更改为 ref.watch
,
这很容易出错,而且你很可能会忘记某些情况。
但如果一开始就使用 ref.watch
,当你重构时遇到的问题就会更少。
但是我想要用 ref.read
来减少小部件重新构建的次数
这个想法很好,但需要注意的是,使用 ref.watch
也可以达到完全相同的效果(减少重新构建的次数)。
provider提供了许多获取值的方法,同时也减少了重新构建的次数,你可以使用这些方法。
比如,不应该这样:
class Counter extends _$Counter {
int build() => 0;
void increment() => state = state + 1;
}
Widget build(BuildContext context, WidgetRef ref) {
Counter counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.increment(),
child: const Text('button'),
);
}
我们应该:
class Counter extends _$Counter {
int build() => 0;
void increment() => state = state + 1;
}
Widget build(BuildContext context, WidgetRef ref) {
Counter counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.increment(),
child: const Text('button'),
);
}
这两段代码实现了相同的效果:当计数器增加时,我们的按钮也不会重新构建。
另一方面,第二种方法支持重置计数器。例如,应用的另一部分可以调用:
ref.refresh(counterProvider);
来重新创建 Counter
对象。
如果我们在这里使用 ref.read
,我们的按钮仍将使用之前的 Counter
实例,
但实际上该实例已被丢弃,不应该再使用。
而正确使用 ref.watch
将重新构建按钮以使用新的 Counter
实例。
选择读取的方式
根据你想要监听的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 参考来获取每个provider的文档。
使用“select”来过滤重建内容
与读取provider相关的最后一个特性是能够减少widget/provider从 ref.watch
重新构建的次数,
或者减少 ref.listen
执行函数的频率。
记住,这一点很重要,因为默认情况下,监听provider将监听整个对象状态。 但有时widget/provider可能只关心某些属性的更改,而不是整个对象。
比如说,一个provider可能暴露一个 User
对象:
abstract class User {
String get name;
int get age;
}
但是widget可能只使用用户名:
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
如果我们简单地使用 ref.watch
,这将在用户年龄(age
)发生变化时重新构建widget。
解决方案是使用 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将调用该函数并比较以前和新的结果。
如果它们是不同的(例如当名称更改时),Riverpod将重新构建widget。
当然了,如果它们相等(例如当年龄改变时),Riverpod将不会重建widget。
也可以 select
和 ref.listen
结合使用:
ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);
这样做只会在名称更改时调用监听器。
你不需要返回对象的属性。任何可以使用==的值都可以工作。举个例子:
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));