原文链接:Provider vs Riverpod | Riverpod
pub:riverpod | Dart Package (flutter-io.cn)
译时版本: 2.4.5
之前翻译过 RiverPod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。
之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)
Provider vs Riverpod
该文章简要描述了 Provider 和 Riverpod 的差异点和相似点。
定义 provider
两个包的主要的差异是定义 "providers" 的方式。
使用 Provider ,provider 都是组件,所以会放置在组件树内部,经典用法是在 MultiProvider
里:
dart
class Counter extends ChangeNotifier {
...
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(create: (context) => Counter()),
],
child: MyApp(),
)
);
}
使用 Riverpod ,provider 不是 组件。它们反而是普通的 Dart 对象。
类似地,provider 是在组件树外部定义的,并声明为全局的 final 变量。
而且,要正常使用 Riverpod ,需要在整个应用上上添加 ProviderScope
组件。 这样,将 Provider 示例改写为使用 Riverpod 会是下面这个样子:
dart
// Provider 现在是顶级的变量
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());
void main() {
runApp(
// 该组件使 Riverpod 在整个工程可用
ProviderScope(
child: MyApp(),
),
);
}
能注意到 provider 只需要简单修改几行。
信息:
由于 Riverpod 的 provider 是普通的 Dart 对象,所以不是 Flutter 开发,也能使用 Riverpod 。
例如,Riverpod 可用于编写命令行应用。
读取 provider : BuildContext
使用 Provider ,读取 provider 的一个方式是使用组件的 BuildContext
。
例如,如果 provider 定义如下:
dart
Provider<Model>(...);
用 Provider 读取的代码会如下:
dart
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
Model model = context.watch<Model>();
}
}
用 Riverpod 读取的话就会是:
dart
final modelProvider = Provider<Model>(...);
class Example extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
Model model = ref.watch(modelProvider);
}
}
注意改写方式:
- Riverpod 的代码片段继承了
ConsumerWidget
而不是StatelessWidget
。不同的组件类型是向build
函数添加了一个变量:WidgetRef
。 - 代替
BuildContext.watch
,在 Riverpod 中是用WidgetRef.watch
,WidgetRef
可从ConsumerWidget
获取。 - Riverpod 不依赖泛型。它依赖用 provider 定义创建的变量。
也要注意二者的用语很相似。Provider 和 Riverpod 都使用关键字 "watch" 描述 "当值改变时该组件需要重绘"。
信息:
Riverpod 使用 Provider 相似的术语用于读取 provider 。
BuildContext.watch
->WidgetRef.watch
->BuildContext.read
->WidgetRef.read
BuildContext.select
->WidgetRef.watch(myProvider.select)
context.watch
vscontext.read
的规则对于 RiverPod 也是适用的: 在build
方法中,使用watch
。在点击处理器或其它事件中,使用read
。 需要过滤值并重绘时,使用select
。
读取 provider :Consumer(消费者)
Provider 带有一个名为 Consumer
(Consumer2
之类的变量)的可选组件用于读取 provider 。
通过允许更多颗粒级的组件树重绘,Consumer
有助于性能优化。
- 只在状态改变时更新关联的组件:
照这样,如果 provider 定义如下:
dart
Provider<Model>(...);
Provider 可如下使用 Consumer
读取这个 provider :
dart
Consumer<Model>(
builder: (BuildContext context, Model model, Widget? child) {
}
)
Riverpod 也有相同的准则。 Riverpod 也有一个名为 Consumer
的组件用于完全相同的处理。
如果定义 provider 如下:
dart
final modelProvider = Provider<Model>(...);
然后使用 Consumer
可以如下这样:
dart
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
Model model = ref.watch(modelProvider);
}
)
注意 Consumer
是如何给我们一个 WidgetRef
对象。 这和在前面看到的 ConsumerWidget
关联的部分是相同的对象。
Riverpod 中没有 ConsumerN
注意 pkg:Provider 中的 Consumer2
,Consumer3
等是如何在 RiverPode 中不需要或者没有的。
使用 Riverpod ,如果需要从多个 provider 中读取值,只需写多个 ref.watch
语句,如下:
dart
Consumer(
builder: (context, ref, child) {
Model1 model = ref.watch(model1Provider);
Model2 model = ref.watch(model2Provider);
Model3 model = ref.watch(model3Provider);
// ...
}
)
相对于 pkg:Provider 的 ConsumerN
API,上面的方案更轻量易懂。
绑定 providers :无状态对象的 ProxyProvider
使用 Provider ,绑定 provider 的官方方式是使用 ProxyProvider
组件(或者 ProxyProvider2
之类的变量)。
例如,我们可能定义如下:
dart
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
这样有两个选项。绑定 UserIdNotifier
创建新的"无状态" provider(典型的是重写 == 的不可变值)。如下:
dart
ProxyProvider<UserIdNotifier, String>(
update: (context, userIdNotifier, _) {
return 'The user ID of the the user is ${userIdNotifier.userId}';
}
)
该 provider 会在 UserIdNotifier.userId
发生改变时自动返回新的 String
。
可以在 RiverPod 中用相同的方式,只是语法有所不同。
首先,在 RiverPod 中,UserIdNotifier
的定义会如下:
dart
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
);
然后生成基于 userId
的 String ,可如下:
dart
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
应该似曾相识。 该模式在前面解释 如何读取组件内部的 provider 时说明过。 实际上 provider 现在也能用使用组件时相同的方式监听其它的 provider 。
绑定 provider :有状态对象的 ProxyProvider
绑定 provider 时,另外一个场景是暴露有状态的对象,如 ChangeNotifier
实例。
对于这种情况,可以使用 ChangeNotifierProxyProvider
(或 ChangeNotifierProxyProvider2
之类的变量)
例如,可能定义如下:
dart
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
然后定义一个新的基于 UserIdNotifier.userId
的 ChangeNotifier
。例如可如下:
dart
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
(永远不会重新构造)的单例,并在 user ID 发生改变时打印字符串。
在 provider 中实现相同处理的方式有所不同。 首先,在 RiverPod 中,UserIdNotifier
的定义会是:
dart
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
然后,前面 ChangeNotifierProxyProvider
需要改写为如下:
dart
class UserNotifier extends ChangeNotifier {
String? _userId;
void setUserId(String? userId) {
if (userId != _userId) {
print('The user ID changed from $_userId to $userId');
_userId = userId;
}
}
}
// ...
final userNotifierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
final userNotifier = UserNotifier();
ref.listen<UserIdNotifier>(
userIdNotifierProvider,
(previous, next) {
if (previous?.userId != next.userId) {
userNotifier.setUserId(next.userId);
}
},
);
return userNotifier;
});
该代码片段的核心是 ref.listen
代码行。
ref.listen
函数是允许监听 provider 的工具函数,当 provider 发生改变时,执行函数。
该函数的 previous
和 next
参数对应着 provider 改变前的最终值和改变后的新值。
作用域 Provider vs .family
+ .autoDispose
在 pkg:Provider 中,作用域用于两种场合:
- 离开页面时清除状态
- 每个页面有各自的自定义状态
使用作用域清除状态不太完美。
问题是作用域在大型应用上并不能很好运用。
例如,状态通常会在某个页面创建,但是可能会在导航跳转到另一个页面后清除。
这样无法为不同的页面操作多个活跃的缓存。
类似地,如果状态需要和组件树的其它部分共享,那"为每个页面自定义状态"的方式很快就会难以驾驭, 例如需要操作模态窗口或者多个步骤的窗体。
Riverpod 采用了不同的方式:首先,不鼓励使用作用域的 provider ;
第二, .family
和 .autoDispose
完全可以代替(作用域)。
在 RiverPod 中,标记为 .autoDispose
的 provider 不再使用时会自动清除它们的状态。
当最后移除 provider 的组件不再装载时,RiverPod 会检测到并清除该 provider。
可以尝试在一个 provider 中使用两个生命周期方法来测试该行为:
dart
ref.onCancel((){
print("No one listens to me anymore!");
});
ref.onDispose((){
print("If I've been defined as `.autoDispose`, I just got disposed!");
});
这能从本质上解决 "清除状态" 的问题。
当然也能将一个 Provider 标记为 .family
(并同时标记为 .autoDispose
)。
这样可以给 provider 传递参数,可以在内部生成多个 provider 并进行追踪。
换句话说,传递参数时,会为每个唯一的参数创建唯一的状态。
dart
@riverpod
int random(RandomRef ref, {required int seed, required int max}) {
return Random(seed).nextInt(max);
}
这解决了 "每个页面都自定义状态" 的问题。实际上,还有另外一个优点:状态不再局限于某个特定的页面。
并且,如果如果不同的页面尝试访问相同的状态,这样的页面也可以只是重新使用参数就能做到。
在很多方面,向 provider 传递参数等同于传递 Map 的键值。 如果键是相同的,获取的值就是相同的。如果是不同的键,就会获取不同的状态。