重学Riverpod(2/2)

843 阅读5分钟

4bf2a95bb3db3a74475ad4132dbd4912.png

上一版本讲了Riverpod的安装,展示helloworld。同时讲了所有5种常用的provider的使用场景和用法。如果还没有学习的同学可以先看第一部,传送门:juejin.cn/post/725189…

本章节主要会讲解剩余的内容,refwatch,read,listen,select,.family,.autoDispose.,看完本章节,明白这些关键字是什么,各自的用途。好了废话不多说,开干。

如何关联使用Provider

首先,在读取提供者之前,我们需要获得一个“ref”对象。这个对象允许我们与provider进行交互,无论是来自widget还是其他provider.

所有的provider都会收到一个ref作为参数,此参数可以安全地传递给提供者公开的值。

final provider = Provider((ref) {  
// 下面可以使用ref去获取其他的providers
final repository = ref.watch(repositoryProvider);  
  
return SomeValue(repository);  
})

例如,一个常见的用例是将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" 去获取其他的 providers
    final repository = ref.read(repositoryProvider);
    repository.post('...');
  }
}

这样做可以让我们的Counter类读取其他的providers,鲜花整起来❀。

那问题来了,widget没有ref参数,widget组件想获取ref怎么操作,Riverpod提供了多种解决方案。

扩展ConsumerWidget替代StatelessWidget

在widget树中获取引用的最常见方法是用ConsumerWidget替换StatelessWidget

ConsumerWidget在使用时与StatelessWidget相同,唯一的区别是,它在构建方法上有一个额外的参数:“ref”对象。 典型的ConsumerWidget看起来像:

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // use ref to listen to a provider
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

扩展ConsumerStatefulWidget+ConsumerState而不是StatefulWidget+State

ConsumerWidget类似,ConsumerStatefulWidgetConsumerState相当于StatefulWidget及其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');
  }
}

我们widget通过以上方式可以获取ref了,那如何交互,目前ref有三种用法。

  1. 获取provider的值并监听更改,当值更改时,将重新构建订阅该值的widget或provider。 用ref.watch.
  2. 在provider序上添加侦听器,以执行操作,例如导航到新页面或在provider更改时显示模态。用ref.listen
  3. 获取provider的值时候同时忽略值可能的更改。当我们在“点击”等事件中需要provider的值时,这很有用。用ref.read

📢注意

只要有可能,官方原文是用prefer using 而不是 suggest using。我理解哈,能用ref.watch咱就别用ref.readref.listen来实现功能。
通过依赖ref.watch,您的应用程序变得既是响应式的又是声明式的,这使其更具可维护性。

使用ref.watch观察provider

ref.watch在Widget的build方法内部或Provider的主体内部使用,用来监听provider.

举个🌰

final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((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;
  }
});

上面例子提供了3个provider,filterTypeProvider是一个公开当前类型过滤器,todosProvider是一个公开整个任务列表。通过使用ref.watch,我们可以创建第三个provider,将两个provider合并,filteredTodoListProvider现在公开了过滤的任务列表。

刚才演示的是在provider内部使用ref.watch。接下来演示在widget内使用的。

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发生变化,widget将重建,UI将更新以显示新值。

⚠️注意: watch方法不应该异步调用,就像在ElevatedButtononPressed中一样。它也不应该在initState和其他生命周期状态内使用。 在这些情况下,请考虑使用ref.read

使用ref.listen对provider更改做出反应

ref.watch类似,可以使用ref.listen来观察provider。

它们之间的主要区别在于,如果被监听的提供程序发生变化而不是重建widget/provider,而是使用ref.listen将调用自定义函数。

*这对于在发生某些变化时执行操作非常有用,例如在发生错误时显示snackbar。

ref.listen方法需要2个位置参数,第一个是provider,第二个是我们在状态更改时想要执行的回调函数。调用回调函数在调用时将传递2个值,前一个状态的值和新状态的值

演示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');
  });
  // ...
});

演示ref.listen方法可以在widget的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方法不应该异步调用,就像在ElevatedButtononPressed中一样。它也不应该在initState和其他生命周期状态内使用。

使用ref.read获取provider的state

ref.read方法是一种在不监听provider的情shi取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: () {
          // Call `increment()` on the `Counter` class
          ref.read(counterProvider.notifier).increment();
        },
      ),
    );
  }
}

⚠️注意: 应尽可能避免使用ref.read,因为它不是响应式的。 不要在构建方法中使用ref.read, 它适用于使用watchlisten会导致问题的情况。如果可以的话,使用watch/listen几乎总是更好,尤其是watch。举个例子:

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

但这是一个非常糟糕的做法,可能会导致难以跟踪的错误。 因为使用ref.read通常provider暴露的值永远不会改变,因此使用ref.read是安全的”。软件往往会改变很多,在未来,一个以前从未改变过的值可能需要改变。
如果您使用ref.read,当该值需要更改时,您必须浏览整个代码库以将ref.read更改为ref.watch——这容易出错,您可能会忘记一些情况。 如果您一开始使用ref.watch,那么在重构时遇到的问题会更少。

但我想使用ref.read来减少我的小部件重建的次数

虽然目标值得称赞,但重要的是要注意,您可以使用ref.watch实现完全相同的效果

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

使用select过滤数据避免重新构建

总结就是:默认情况下,监听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发生变化时重建。

解决方案是使用select明确告诉Riverpod,我们只想监听User的名称属性。

更新的代码是:

Widget build(BuildContext context, WidgetRef ref) {
  String name = ref.watch(userProvider.select((user) => user.name));
  return Text(name);
}

通过使用select,我们能够指定一个函数来返回我们关心的属性。

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

好了ref的操作符到此告一段落,喝杯咖啡。来继续学习修饰符。

修饰符 .family

.family修饰符有一个目的:基于外部参数获取一个唯一的provider。

例如,我们可以将familyFutureProvider结合起来,根据id值获取一条对应的message.

final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

使用我们的messagesFamily提供程序时,语法略有不同。
通常的语法将不再起作用:

Widget build(BuildContext context, WidgetRef ref) {
  // Error – messagesFamily is not a provider
  final response = ref.watch(messagesFamily);
}

相反,我们需要将一个参数传递给messagesFamily

Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
}

参数限制

为了使families正常工作,传递给提供者的参数必须具有一致的hashCode==

理想情况下,参数应该是原始(bool/int/double/String)、常量(提供者)或覆盖==hashCode的不可变对象。

当参数不恒定时 ,更喜欢使用autoDispose

您可能希望使用families将搜索字段的输入传递给您的提供商。但这种值可以经常改变,而且永远不会重复使用。
这可能会导致内存泄漏,因为默认情况下,即使不再使用,提供程序也不会被销毁。

使用.family.autoDispose修复内存泄漏:

final characters = FutureProvider.autoDispose.family<List<Character>, String>((ref, filter) async {
  return fetchCharacters(filter: filter);
});

修饰符 .autoDispose

通常作用:是在不再使用providr时销毁它的状态。

用法

要告诉Riverpod在不再使用时销毁提供商的状态,只需将.autoDispose附加到您的提供商:

final userProvider = StreamProvider.autoDispose<User>((ref) {

});

就是这样。现在,当userProvider不再使用时,其状态将自动销毁。 如果您需要,您可以将.autoDispose与其他修饰符组合:

final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {

});

ref.keepAlive

使用autoDispose标记提供商还会在ref上添加一个额外的方法:keepAlive

keepAlive函数用于告诉Riverpod,即使不再监听,也应保留提供商的状态。

用例是在HTTP请求完成后将此标志设置为true

final myProvider = FutureProvider.autoDispose((ref) async {
  final response = await httpClient.get(...);
  ref.keepAlive();
  return response;
});

这样,如果请求失败,用户离开屏幕然后重新输入,则将再次执行请求。但是,如果请求成功完成,状态将被保留,重新进入屏幕不会触发新的请求。

结束~