Skip to content

Provider 的“家族”:Family

一句话总结

“Family” 是 Riverpod 最强大的功能之一,它让一个 Provider 能同时管理多个独立的状态。


“Family” (家族) 是 Riverpod 最强大的功能之一。

简单来说,它允许一个 Provider 根据一个独特的参数,关联到多个相互独立的状态。

一个典型的应用场景就是从服务器获取数据,比如根据用户 ID 获取用户信息、根据关键词获取搜索结果、或者根据页码获取列表数据。在这些场景下,响应的数据都依赖于一个或多个参数。Family 让你只需定义一个 Provider,就能搞定所有不同参数下的数据获取和缓存。

一张图表,展示了 family 如何将一个 provider 关联到多个独立的状态

小贴士

如果说普通的 Provider 像一个“变量”,那么带 Family 的 Provider 就像一个 Map (字典)。

如何创建一个 Family

定义一个 Family,只需要对 Provider 的定义稍作修改,让它能接收一个参数即可。

对于函数式的 Provider,语法如下:

dart
@riverpod
Future<User> user(
  Ref ref,
  // 使用代码生成时,provider 可以接收任意数量的参数。
  // 这些参数可以是位置参数/命名参数,也可以是必选/可选的。
  String id,
) async {
  final dio = Dio();
  final response = await dio.get('https://api.example.com/users/$id');

  return User.fromJson(response.data);
}

对于 Notifier 类型的 Provider,语法是这样的:

dart
@riverpod
class UserNotifier extends _$UserNotifier {
  @override
  Future<User> build(
    // 使用代码生成时,Notifier 可以在 "build" 方法上定义参数。
    // 可以定义任意数量的参数。
    String id,
  ) async {
    final dio = Dio();
    final response = await dio.get('https://api.example.com/users/$id');

    // 生成的类中,可以自然地通过 "this" 访问到传递给 "build" 方法的参数:
    print(this.id);

    return User.fromJson(response.data);
  }
}

小贴士

虽然不是强制性的,但我们强烈建议在使用 Family 时,开启自动销毁功能。

这样可以避免当参数改变,旧的状态不再需要时,发生内存泄漏。

如何使用一个 Family

带参数的 Provider,在使用时也需要稍作改变。

长话短说,你需要像调用函数一样,把你的 Provider 期望的参数传给它:

dart
final user = ref.watch(userProvider('123'));

注意

传递的参数需要有一个稳定可靠的 ==hashCode 实现。

你可以把 “Family” 看作一个 Map,其中参数是 key,Provider 的状态是 value。因此,如果参数的 == / hashCode 变了,你得到的值也会不同。

所以,像下面这样的代码是错误的:

dart
// 错误的参数,因为 `[1, 2, 3] != [1, 2, 3]`
ref.watch(myProvider([1, 2, 3]));

为了帮助发现这个错误,建议使用 riverpod_lint 并开启 provider_parameters 这条 lint 规则。这样,上面的代码片段就会显示一个警告。安装步骤请参见入门指南

你可以读取任意多个带 Family 的 Provider,它们之间都是相互独立的。所以,这样做是完全合法的:

dart
class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user1 = ref.watch(userProvider('123'));
    final user2 = ref.watch(userProvider('456'));

    // user1 和 user2 是相互独立的。
  }
}

在测试中覆盖 Family

在写测试时,你可能想要模拟一个带 Family 的 Provider。

在这种情况下,你有两个选择:

  • 只覆盖一个特定的参数组合:

    dart
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          // 只模拟 id 为 '123' 的情况
          userProvider('123').overrideWith((ref) => User(name: 'User 123')),
        ],
        child: const MyApp(),
      ),
    );
  • 覆盖所有的参数组合:

    dart
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          // 模拟所有 id 的情况
          userProvider.overrideWith((ref, id) => User(name: 'User $id')),
        ],
        child: const MyApp(),
      ),
    );