跳到主要内容

读取 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 类似, ConsumerStatefulWidgetConsumerState 等价于带有状态的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.readref.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将更新以显示新的值。

警告

像在 ElevatedButtononPressed 中那样,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();
}
}
警告

像在 ElevatedButtononPressed 中那样,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 ,因为它不是响应式的。

它的存在是由于使用 watchlisten 会导致问题。如果可以,使用 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我们只想监听 Username 属性。

更新后的代码如下:

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。

信息

也可以 selectref.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}'));