Skip to content

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:

dart
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 来代替。它会在测试结束时自动帮你销毁容器。

dart
test('计数器从 0 开始,并且可以递增', () {
  // 测试结束时无需手动销毁容器
  final container = ProviderContainer.test();

  // 使用这个容器来测试你的 provider
});

在 Flutter 应用中使用 ProviderScope ​

在 Flutter 应用中,你不应该直接使用 ProviderContainer。取而代之的是,你应该使用 ProviderScope,它是一个等效于 ProviderContainer 的 Widget。

最终的效果是一样的:在你的 main 函数里创建一个 ProviderScope。之后,你就可以在你的 Widget 中使用 Consumer 来读取和修改 Provider 了。

dart
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 不自己存储自己的状态呢?如果我们去掉这个要求,我们就可以想象一个这样的世界:

dart
// 直接访问 provider 的值
print(helloWorldProvider.value); // 打印 "Hello world!"

而不用像现在这样,必须写 ref.watch(helloWorldProvider)。

Riverpod 这么设计有几个原因,它们都归结于同一个理念:“拒绝全局状态”。

  1. 更好的关注点分离。

    如果 Riverpod 允许 Provider 自己存储状态,那就意味着任何地方的代码都可以读取或写入这个状态。这会导致很难控制一个状态在何时、以何种方式被修改。

    而在 Riverpod 的架构下,状态的更新是集中管理的:所有修改一个 Provider 的逻辑都在这个 Provider 内部完成。通常,UI 只会调用 Provider 的 Notifier 上的一个方法来触发更新。

  2. 更好的测试体验。

    通过将 Provider 的状态存储在容器中,我们就不必在不同测试之间操心如何重置应用的状态了。我们只需为每个测试创建一个新的容器,每个 Provider 都会得到一个全新的、干净的状态:

    dart
    test('计数器从 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 测试时,同样的道理也适用。

  3. 一个集中配置应用的地方。

    通过 ProviderContainer 和 ProviderScope,我们可以配置 Riverpod 的各种应用级行为。例如:

    • 我们可以定义一个自定义的 ProviderObserver 来监听应用中所有的状态变化。
    • 我们可以局部或全局地覆盖(override)Provider。这对于测试、或者为不同环境(如开发、生产)提供不同配置的应用非常有用。
  4. 支持范围限定的 Provider。

    通过将 Provider 的状态存储在容器中,我们可以让同一个 Provider 根据它在 Widget 树中所处的位置,解析出不同的状态。这个功能相当高级,通常不鼓励使用,但对于性能优化很有用。