给Provider开发者的Riverpod指南
本文是为熟悉 Provider 并希望了解Riverpod的开发者所设计的。
Riverpod 和 Provider 之间的关系
Riverpod可以说是Provider的“精神”继承者。而且“Riverpod”这个名字就是“Provider”的变位词。
Riverpod是在寻找解决Provider所面临的技术限制的方案中诞生的。 一开始Riverpod被认为是解决这一问题的Provider的下一个大版本。 但最后并没有这么做,因为这样会是一个相当大的突破性变更, 而Provider是现如今最常用的Flutter package 之一。
当然了,Riverpod和Provider从概念上来说还是差不多的。 两个package都扮演着类似的角色。两者都试图:
- 缓存和小会一些有状态的对象
- 在测试时提供一个模拟这些对象的方法
- 在flutter widget中提供了一个简单的方式来监听这些对象的变化
与此同时,如果Riverpod继续成熟下去,它也可能会成为Provider。
Riverpod修复了Provider的许多基本问题,例如但不限于:
- 显著简化了与“provider”的组合。
Riverpod提供了简单强大的工具比如 ref.watch 和 ref.listen ,
而不是冗长且容易出错的
ProxyProvider
。 - 允许多个“Provider”公开相同类型的值。 当你暴露一个int类型或String类型的provider时不需要自定义一个类也能完美地使用。
- 不需要在测试中重新定义provider。 在Riverpod中默认情况下provider可以直接使用。
- 通过提供一种替代的方法来销毁对象(autoDispose),减少对“作用域”的过度依赖。 虽然很强大,但确定一个provider的作用域相当复杂而且很难做对。
还有更多……
Riverpod唯一的缺点就是他需要变更Widget继承的类型才能使用:
- 在Riverpod中你应该扩展
ConsumerWidget
类,而不是StatelessWidget
类。 - 在Riverpod中你应该扩展
ConsumerStatefulWidget
类,而不是StatefulWidget
类。
但从大局上来看这种不便利的影响还是比较小的。而且这个限制在未来可能被移除。
所以要回答这个问题,你可能会问自己: 我要使用Provider 还是 Riverpod ?
你大概率会选择 Riverpod。 Riverpod设计得更好还能急剧简化你的逻辑。
Provider和Riverpod的差异
定义 providers
两个package最主要的区别在于如何定于“provider”。
对 Provider 来说,provider是widget,因此被放在widget树中,一般位于 MultiProvider
中:
class Counter extends ChangeNotifier {
...
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(create: (context) => Counter()),
],
child: MyApp(),
)
);
}
对于Riverpod来说, provider 不是 widget 而是普通的Dart对象。 类似地,provider定义在widget树之外,并声明为全局final变量。
另外,为了让Riverpod工作,应当在整个应用上方添加一个 ProviderScope
widget。
下面是Riverpod的provider示例:
// Providers 现在是顶级变量
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());
void main() {
runApp(
// 这个 widget 可以让Riverpod在整个Flutter 项目中使用。
ProviderScope(
child: MyApp(),
),
);
}
provider的定义只是向上移动了几行。
由于Riverpod的provider是普通的Dart对象,因此可以同时在Dart和Flutter使用Riverpod。 例如Riverpod也可以编写命令行应用。
读取provider: BuildContext
当使用Provider时,读取provider的一种方法是使用widget的 BuildContext
.
例如,如果provider被定义为:
Provider<Model>(...);
然后使用 Provider 读取它:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
Model model = context.watch<Model>();
}
}
Riverpod 的情况也一样:
final modelProvider = Provider<Model>(...);
class Example extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
Model model = ref.watch(modelProvider);
}
}
注意:
Riverpod的代码段展示了其扩展自
ConsumerWidget
类而不是StatelessWidget
类。 它在build函数中添加了一个额外的参数:WidgetRef
。不同于
BuildContext.watch
,我们在Riverpod中使用来自ConsumerWidget
的WidgetRef
来调用WidgetRef.watch
。Riverpod不依赖泛型类型。相反,它依赖于使用的provider定义创建的变量。
它们的使用关键词很相似。Provider和Riverpod都适用关键字“watch”来描述“当值发生变化时,这个widget应该重新构建”。
Riverpod使用和Provider相同的术语来读取provider。
BuildContext.watch
->WidgetRef.watch
BuildContext.read
->WidgetRef.read
context.watch
和 context.read
的规则也适用于Riverpod:
在 build
方法中使用 “watch”。在点击或其他事件中使用 “read”。
读取 provider: Consumer
Provider附带了一个名为 Consumer
(以及 Consumer2
之类的变体) 的widget用来读取provider。
Consumer
对性能优化很有帮助,它允许更细粒度的widget树重新构建,在状态变更时只更新相关的widget:
因此,如果provider被定义为:
Provider<Model>(...);
Provider允许你通过 Consumer
读取provider:
Consumer<Model>(
builder: (BuildContext context, Model model, Widget? child) {
}
)
Riverpod也有这样的原则。Riverpod也有一个名为 Consumer
的相同目的的widget。
如果我们定义provider为:
final modelProvider = Provider<Model>(...);
然后使用 Consumer
我们可以这样:
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
Model model = ref.watch(modelProvider);
}
)
请注意这个 Consumer
是如何提供给我们 WidgetRef
对象的。
这和我们在前面与 ConsumerWidget
相关的内容中看到的对象相同。
组合 provider: 带有无状态对象的 ProxyProvider
当使用Provider,当组合不同的provider时官方的方法是使用 ProxyProvider
widget (或者使用比如 ProxyProvider2
的变体)。
比如说,我们定义了:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
接下来我们有两个选择:
我们可能结合 UserIdNotifier
来创建一个新的 “无状态(stateless)” 的provider(通常是一个重载了==方法的不可变的值), 比如:
ProxyProvider<UserIdNotifier, String>(
update: (context, userIdNotifier, _) {
return 'The user ID of the the user is ${userIdNotifier.userId}';
}
)
每当 UserIdNotifier.userId
出现变化时provider会自动返回一个新的 String
的值。
我们可以在Riverpod中做类似的事,但是语法不同。
首先,在Riverpod中 UserIdNotifier
的定义是:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
然后我们就可以根据 userId
生成我们的 String
:
final labelProvider = Provider<String>((ref) {
UserIdNotifier userIdNotifier = ref.watch(userIdNotifierProvider);
return 'The user ID of the the user is ${userIdNotifier.userId}';
});
注意 ref.watch(userIdNotifierProvider)
这一行。
这行代码告诉Riverpod获取 userIdNotifierProvider
的内容,
并当值发生变化时, labelProvider
也将被重新计算。
因此,当 userId
发生变化时,我们的 labelProvider
将自动更新 String
。
ref.watch
这一行你应该感觉类似。
这种模式在前面的如何在widget中读取provider已经解释过了。
实际上,provider现在可以像widget一样监听其他provider。
组合provider: 带有有状态对象的ProxyProvider
在组合provider时,另一个可选的替代的用法是暴露一个有状态的对象,比如 ChangeNotifier
实例。
为此我们可以使用 ChangeNotifierProxyProvider
(如 ChangeNotifierProxyProvider2
等变体)。
比如我们可能定义:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
然后,我们可以定义一个基于UserIdNotifier.userId
的新 ChangeNotifier
。举个例子:
class UserNotifier extends ChangeNotifier {
String? _userId;
void setUserId(String? userId) {
if (userId != _userId) {
print('The user ID changed from $_userId to $userId');
_userId = userId;
}
}
}
// ...
ChangeNotifierProxyProvider<UserIdNotifier, UserNotifier>(
create: (context) => UserNotifier(),
update: (context, userIdNotifier, userNotifier) {
return userNotifier!
..setUserId(userIdNotifier.userId);
},
);
这个新的provider创建一个 UserNotifier
实例(并且永远不会重新构造),并在用户ID更改时打印一个字符串。
在Riverpod中,这个功能的实现方式不同于provider。首先,我们的 UserIdNotifier
定义为:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
接着,与前面的 ChangeNotifierProxyProvider
等价的是:
class UserNotifier extends ChangeNotifier {}
final userNotfierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
final userNotifier = UserNotifier();
ref.listen<UserIdNotifier>(
userIdNotifierProvider,
(previous, next) {
if (previous?.userId != next.userId) {
print('The user ID changed from ${previous?.userId} to ${next.userId}');
}
},
);
return userNotifier;
});
这段代码的核心是 ref.listen
这一行。
ref.listen
方法是一个允许你监听其他provider和当provider变更时执行函数的工具。
该函数的 previous
和 next
参数对应provider 更改前的最后一个值和更改后的新值。