Provider 对比 Riverpod
本文将介绍 Provider 和 Riverpod 之间的差异和相似之处。
定义提供者程序
这两个包之间的主要区别在于如何定义“提供者程序”。
对于 Provider,提供者程序是小部件,因此放置在小部件树中,通常位于 MultiProvider
:
class Counter extends ChangeNotifier {
...
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(create: (context) => Counter()),
],
child: MyApp(),
)
);
}
使用 Riverpod,提供者程序不是小部件。相反,它们是普通的 Dart 对象。
同样,提供者程序在小部件树之外定义,而且声明为全局 final 变量。
此外,要使 Riverpod 正常工作,必须在整个应用程序上方添加一个小 ProviderScope
部件。
因此,使用 Riverpod 和 Provider 示例等效的版本为:
// provider 现在是顶级变量
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());
void main() {
runApp(
// 该小部件为整个项目启用了 Riverpod
ProviderScope(
child: MyApp(),
),
);
}
请注意,这个 ChangeNotifierProvider 的定义只是向上移动了几行。
由于 Riverpod 的提供者程序是普通的 Dart 对象,因此可以在没有 Flutter 的情况下使用 Riverpod。
例如,Riverpod 可用于编写命令行应用程序。
读取提供者程序:使用 BuildContext
使用 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
。
- 在 Riverpod 中我们使用
WidgetRef.watch
代替BuildContext.watch
,WidgetRef
是我们从ConsumerWidget
拿到的。
- Riverpod 不依赖于泛型类型。相反,它依赖于使用提供者程序定义创建的变量。
还要注意措辞的相似程度。Provider 和 Riverpod 都使用关键字“watch”来描述“当值更改时,这里的小部件应重新生成”。
Riverpod 使用与 Provider 相同的术语来读取提供者程序。
BuildContext.watch
->WidgetRef.watch
BuildContext.read
->WidgetRef.read
BuildContext.select
->WidgetRef.watch(myProvider.select)
context.watch
相对于 context.read
的规则也适用于 Riverpod:
在 build
方法中,使用 “watch”。在单击处理程序和其他事件中,使用 “read”。
当需要过滤掉值并重新生成时,请使用 “select”。
读取提供者程序:使用 Consumer
Provider 可以选择附带一个名为 Consumer
(以及名为 Consumer2
的变体)的小部件,用于读取提供者程序。
Consumer
作为性能优化很有帮助,它允许对小部件树进行更精细的重建 - 在状态更改时仅更新相关的小部件:
因此,如果一个 provider 被定义为:
Provider<Model>(...);
Provider 允许使用 Consumer
读取这个 provider:
Consumer<Model>(
builder: (BuildContext context, Model model, Widget? child) {
}
)
Riverpod 也有同样的原理。Riverpod 也有一个以完全相同功能的 Consumer
小部件。
如果我们将一个 provider 定义为:
final modelProvider = Provider<Model>(...);
然后我们可以使用 Consumer
实现:
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
Model model = ref.watch(modelProvider);
}
)
注意 Consumer
是如何给我们一个 WidgetRef
对象。这与我们在上一部分中看到的 ConsumerWidget
与相关的对象相同。
Riverpod 中没有 ConsumerN
等效的类
请注意,在 Riverpod 中不需要 pkg:Provider 的 Consumer2
、Consumer3
等,也不要遗漏重构它们。
使用 Riverpod,如果要从多个提供者程序读取值,只需编写多个 ref.watch 语句即可,如下所示:
Consumer(
builder: (context, ref, child) {
Model1 model = ref.watch(model1Provider);
Model2 model = ref.watch(model2Provider);
Model3 model = ref.watch(model3Provider);
// ...
}
)
与 pkg:Provider 的 ConsumerN
API 相比,上述解决方案感觉不那么沉重,应该更容易理解。
组合提供者程序:ProxyProvider 与无状态对象
使用 Provider 时,组合提供者程序的官方方法是使用 ProxyProvider
widget(或变体,例如 ProxyProvider2
)。
例如,我们可以定义:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
在这里我们有两个选择。我们可以组合 UserIdNotifier
创建一个新的“无状态”提供者程序
(通常是一个可能覆盖 == 的不可变值)。如:
ProxyProvider<UserIdNotifier, String>(
update: (context, userIdNotifier, _) {
return 'The user ID of the the user is ${userIdNotifier.userId}';
}
)
每当 UserIdNotifier.userId
发生更改时,这个提供者程序都会自动返回新的 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
应该感觉很熟悉。
之前在解释如何在小部件中读取提供者程序时已经介绍了这个模式。
事实上,提供者程序现在能够与小部件以相同的方式监听其他提供者程序的改变。
组合提供者程序:ProxyProvider 与有状态对象
组合提供者程序时,另一个替代用例是公开有状态对象,例如 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);
},
);
这个新提供者程序会创建一个 UserNotifier
实例(它永远不会重新构造),
并在用户 ID 更改时打印一个字符串。
在提供者程序中执行相同的操作是以不同的方式实现的。
首先,在 Riverpod 中,我们的 UserIdNotifier
是这样定义的:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
相比于上面 ChangeNotifierProxyProvider
的等价代码将是:
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
函数是一个实用程序,它允许监听一个提供者程序,
并在提供者程序更改时执行函数。
该函数的 previous
和 next
参数对应于提供者程序更改前的最后一个值和更改后的新值。
作用域提供者程序与 .family
+ .autoDispose
在 pkg:Provider 中,作用域用于两件事:
- 离开页面时处置状态
- 每页具有自定义状态
仅使用作用域来破坏状态并不理想。
问题在于,作用域在大型应用程序上效果不佳。
例如,状态通常在一个页面中创建,但在导航后稍后在另一个页面中处置。
这不允许多个缓存在不同的页面上处于活动状态。
同样,如果需要与小部件树的另一部分共享状态, “自定义每个页面状态”的方法很快就会变得难以处理, 就像你需要模态或多步骤表单一样。
Riverpod 采取了不同的方法:首先,不鼓励使用作用域提供者;
其次, .family
和 .autoDispose
是完整的替代解决方案。
在 Riverpod 中,当一个提供者程序标记为 .autoDispose
在不再使用时会自动处置的状态。
当卸载最后一个删除提供者程序的小部件时,Riverpod 将检测到卸载并处置提供者程序。
尝试在提供者程序中使用以下两种生命周期方法来测试此行为:
ref.onCancel((){
print("我一个监听程序都没有了!");
});
ref.onDispose((){
print("如果我已经被定义为 `.autoDispose`,我将被处置!");
});
这从本质上解决了“破坏状态”问题。
此外,还可以将提供者程序标记为 .family
(同时,也可以标记为 .autoDispose
)。
这样就可以将参数传递给提供者程序,从而在内部生成和跟踪多个提供者程序。
换句话说,在传递参数时,会为每个唯一参数创建一个唯一状态。
int random(Ref ref, {required int seed, required int max}) {
return Random(seed).nextInt(max);
}
这解决了“每页自定义状态”问题。实际上,还有另一个优点:这种状态不再绑定到一个特定的页面。
相反,如果不同的页面尝试访问相同的状态,则该页面只需重用参数即可实现。
在许多方面,将参数传递给提供者程序等同于 Map 的键。
如果键相同,则获取的值相同。如果是不同的键,则将获得不同的状态。