读取 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}'));