跳到主要内容

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.watchWidgetRef 是我们从 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 的 Consumer2Consumer3 等,也不要遗漏重构它们。

使用 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 函数是一个实用程序,它允许监听一个提供者程序, 并在提供者程序更改时执行函数。

该函数的 previousnext 参数对应于提供者程序更改前的最后一个值和更改后的新值。

作用域提供者程序与 .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 的键。
如果键相同,则获取的值相同。如果是不同的键,则将获得不同的状态。