Provider 的“大本营”:ProviderContainer 与 ProviderScope
一句话总结
ProviderContainer是 Riverpod 架构的绝对核心,它像一个大容器,装着所有 Provider 的状态。
ProviderContainer 是 Riverpod 架构的绝对核心。
在 Riverpod 中,Provider 自己本身是不存储任何状态的。相反,一个 Provider 的状态,实际上是存放在这个 ProviderContainer 对象里的。
ProviderScope 则是一个 Widget,它的作用就是创建一个 ProviderContainer,并把它暴露给整个 Widget 树。这就是为什么,当你使用 Riverpod 时,总会看到一个 ProviderScope 包裹在应用的根部。
没有它,Riverpod 就没地方存放 Provider 的状态了!
在纯 Dart 应用中使用 ProviderContainer
当你想在纯 Dart 代码库(比如命令行应用或服务器端应用)中使用 Riverpod 时,ProviderContainer 就非常有用了。
你可以在你的 main 函数里创建一个 ProviderContainer,然后用它来读取和修改 Provider:
import 'package:riverpod/riverpod.dart';
void main() {
// 创建一个容器
final container = ProviderContainer();
try {
// 监听 counterProvider 的变化
final sub = container.listen(counterProvider, (previous, next) {
print('计数器从 $previous 变成了 $next');
});
// 读取初始值
print('计数器初始值为 ${sub.read()}');
} finally {
// 完成后,销毁容器以释放资源
container.dispose();
}
}注意
在写测试时,不要直接使用
ProviderContainer。你应该使用ProviderContainer.test来代替。它会在测试结束时自动帮你销毁容器。darttest('计数器从 0 开始,并且可以递增', () { // 测试结束时无需手动销毁容器 final container = ProviderContainer.test(); // 使用这个容器来测试你的 provider });
在 Flutter 应用中使用 ProviderScope
在 Flutter 应用中,你不应该直接使用 ProviderContainer。取而代之的是,你应该使用 ProviderScope,它是一个等效于 ProviderContainer 的 Widget。
最终的效果是一样的:在你的 main 函数里创建一个 ProviderScope。之后,你就可以在你的 Widget 中使用 Consumer 来读取和修改 Provider 了。
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
runApp(
ProviderScope(
child: Consumer(
builder: (context, ref, _) {
final counter = ref.watch(counterProvider);
// TODO: 使用 "counter"
},
),
),
);
}为什么要用一个“容器”来存储 Provider 的状态?
有人可能会想,为什么 Provider 不自己存储自己的状态呢?如果我们去掉这个要求,我们就可以想象一个这样的世界:
// 直接访问 provider 的值
print(helloWorldProvider.value); // 打印 "Hello world!"而不用像现在这样,必须写 ref.watch(helloWorldProvider)。
Riverpod 这么设计有几个原因,它们都归结于同一个理念:“拒绝全局状态”。
更好的关注点分离。
如果 Riverpod 允许 Provider 自己存储状态,那就意味着任何地方的代码都可以读取或写入这个状态。这会导致很难控制一个状态在何时、以何种方式被修改。
而在 Riverpod 的架构下,状态的更新是集中管理的:所有修改一个 Provider 的逻辑都在这个 Provider 内部完成。通常,UI 只会调用 Provider 的 Notifier 上的一个方法来触发更新。
更好的测试体验。
通过将 Provider 的状态存储在容器中,我们就不必在不同测试之间操心如何重置应用的状态了。我们只需为每个测试创建一个新的容器,每个 Provider 都会得到一个全新的、干净的状态:
darttest('计数器从 0 开始,并且可以递增', () { final container = ProviderContainer.test(); expect(container.read(counterProvider), 0); container.read(counterProvider.notifier).increment(); expect(container.read(counterProvider), 1); }); test('计数器不能低于 0', () { final container = ProviderContainer.test(); expect(container.read(counterProvider), 0); container.read(counterProvider.notifier).decrement(); expect(container.read(counterProvider), 0); });在这里,我们可以看到两个测试都依赖于同一个
counterProvider。然而,一个测试中的状态变化并不会影响到另一个测试。当然,当使用
ProviderScope和 Widget 测试时,同样的道理也适用。一个集中配置应用的地方。
通过
ProviderContainer和ProviderScope,我们可以配置 Riverpod 的各种应用级行为。例如:- 我们可以定义一个自定义的
ProviderObserver来监听应用中所有的状态变化。 - 我们可以局部或全局地覆盖(override)Provider。这对于测试、或者为不同环境(如开发、生产)提供不同配置的应用非常有用。
- 我们可以定义一个自定义的
支持范围限定的 Provider。
通过将 Provider 的状态存储在容器中,我们可以让同一个 Provider 根据它在 Widget 树中所处的位置,解析出不同的状态。这个功能相当高级,通常不鼓励使用,但对于性能优化很有用。