上一版本讲了Riverpod的安装,展示helloworld。同时讲了所有5种常用的provider的使用场景和用法。如果还没有学习的同学可以先看第一部,传送门:juejin.cn/post/725189…
本章节主要会讲解剩余的内容,ref,watch,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类似,ConsumerStatefulWidget和ConsumerState相当于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有三种用法。
- 获取provider的值并监听更改,当值更改时,将重新构建订阅该值的widget或provider。 用
ref.watch. - 在provider序上添加侦听器,以执行操作,例如导航到新页面或在provider更改时显示模态。用
ref.listen。 - 获取provider的值时候同时忽略值可能的更改。当我们在“点击”等事件中需要provider的值时,这很有用。用
ref.read。
📢注意
只要有可能,官方原文是用prefer using 而不是 suggest using。我理解哈,能用
ref.watch咱就别用ref.read或ref.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方法不应该异步调用,就像在ElevatedButton的onPressed中一样。它也不应该在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方法不应该异步调用,就像在ElevatedButton的onPressed中一样。它也不应该在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, 它适用于使用watch或listen会导致问题的情况。如果可以的话,使用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,我们能够指定一个函数来返回我们关心的属性。
也可以将
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}'));
好了ref的操作符到此告一段落,喝杯咖啡。来继续学习修饰符。
修饰符 .family
.family修饰符有一个目的:基于外部参数获取一个唯一的provider。
例如,我们可以将
family与FutureProvider结合起来,根据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;
});
这样,如果请求失败,用户离开屏幕然后重新输入,则将再次执行请求。但是,如果请求成功完成,状态将被保留,重新进入屏幕不会触发新的请求。
结束~