跳到主要内容

组合 Provider 状态

请确保先阅读有关 Providers 的内容。
在本指南中,我们将学习如何组合provider的状态。

组合 Provider 状态

我们之前已经了解了如何创建简单的provider。 但实际在许多情况下,provider想要读取另一个provider的状态。

为此,我们可以使用传递给provider回调函数的e ref 对象,并使用它的 watch 方法。

例如,思考一下下面的provider:

final cityProvider = Provider((ref) => 'London');

We can now create another provider that will consume our cityProvider: 现在我们可以创建另一个provider来使用我们的 cityProvider

final weatherProvider = FutureProvider((ref) async {
// 我们使用 `ref. watch` 来监听另一个provider,
// 我们将使用的cityProvider传递给它。
final city = ref.watch(cityProvider);

// 然后,我们可以根据 `cityProvider` 的值来做一些事情。
return fetchWeather(city: city);
});

这样。我们创建了一个依赖于另一个provider的provider。

常见问题解答

如果被监听的值随着时间的推移而改变怎么办?

例如,你可能正在监听的 StateNotifierProvider 或 已经强制刷新(ProviderContainer.refresh / ref.refresh)过的provider被监听时。

当使用 watch 时,Riverpod能够检测到被监听的值发生了变化 并且在需要时将自动重新执行provider的回调函数。

这在计算状态时很有用。 例如一个 StateNotifierProvider 暴露了一个待办清单:

class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}

final todoListProvider = StateNotifierProvider((ref) => TodoList());

一个常见的用例是让UI过滤待办清单列表,只显示完成/未完成的待办事项。

实现这种场景的一个简单方法是:

  • 创建一个 StateProvider并暴露当前选择的筛选方法:

    enum Filter {
    none,
    completed,
    uncompleted,
    }

    final filterProvider = StateProvider((ref) => Filter.none);
  • 创建一个单独的,结合了筛选器方法和待办清单列表的provider,以暴露经过筛选的待办清单列表:

    final filteredTodoListProvider = Provider<List<Todo>>((ref) {
    final filter = ref.watch(filterProvider);
    final todos = ref.watch(todoListProvider);

    switch (filter) {
    case Filter.none:
    return todos;
    case Filter.completed:
    return todos.where((todo) => todo.completed).toList();
    case Filter.uncompleted:
    return todos.where((todo) => !todo.completed).toList();
    }
    });

然后,我们的UI可以监听 filteredTodoListProvider 来监听过滤后的待办清单。 使用这种方法,当过滤器或待办清单列表发生变化时,UI将自动更新。

要查看这种方法的详细内容,你可以查看 待办清单示例的源代码。

信息

这种行为不只针对 Provider,它适用于所有的provider。

For example, you could combine watch with FutureProvider to implement a search feature that supports live-configuration changes: 例如,你可以将 watchFutureProvider 结合来实现一个支持实时配置更改的搜索功能:

// 当前的搜索条件
final searchProvider = StateProvider((ref) => '');

/// 可随时间变化的配置
final configsProvider = StreamProvider<Configuration>(...);

final charactersProvider = FutureProvider<List<Character>>((ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configsProvider.future);
final response = await dio.get('${configs.host}/characters?search=$search');

return response.data.map((json) => Character.fromJson(json)).toList();
});

这段代码将从服务中获取一个字符列表,并在配置更改或搜索查询更改时自动重新获取该列表。

我可以在不监听的情况下读取provider吗?

有时,我们希望读取provider的内容,但不需要在那个值发生变化时重新创建暴露的值。

一个例子就是Repository,它从另一个provider读取用户令牌进行身份验证。 我们可以在用户令牌更改时使用 watch 并创建一个新的 Repository ,但这样做几乎没有任何用处。

在这种情况下,我们可以使用 read,它类似于 watch, 但是当获取的值发生变化时,它不会导致provider重新创建它所暴露的值。

In that case, a common practice is to pass the provider's Ref to the object created. The object created will then be able to read providers whenever it wants. 所以在这种情况,常见的做法是将provider的 Ref 传递给创建的对象。 创建的对象将能够在任何需要的时候读取提供程序。

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider(Repository.new);

class Repository {
Repository(this.ref);

final Ref ref;

Future<Catalog> fetchCatalog() async {
String token = ref.read(userTokenProvider);

final response = await dio.get('/path', queryParameters: {
'token': token,
});

return Catalog.fromJson(response.data);
}
}
不要在provider的里面调用 read 方法
final myProvider = Provider((ref) {
// 在此处调用“read”的错误做法
final value = ref.read(anotherProvider);
});

如果你使用 read 来避免不必要的对象重建, 请参考我的provider更新过于频繁我该怎么办

如何测试一个接收 ref 作为其构造函数参数的对象?

如果你正在使用我可以在不监听的情况下读取provider吗?中描述的模式, 你可能想知道如何为对象编写测试。

在这个场景中,考虑直接测试provider而不是原始对象。你可以通过使用 ProviderContainer 类来实现:

final repositoryProvider = Provider((ref) => Repository(ref));

test('fetches catalog', () async {
final container = ProviderContainer();
addTearDown(container.dispose);

Repository repository = container.read(repositoryProvider);

await expectLater(
repository.fetchCatalog(),
completion(Catalog()),
);
});

我的provider更新过于频繁,我该怎么办?

如果你的对象被频繁地重新创建,你的provider可能会监听它并不关心的对象。

例如,你可能正在监听一个 Configuration 对象,但只使用 host 属性。
通过监听整个 Configuration 对象,如果 host 以外的某个属性发生了变化, 仍然会导致重新评估你的provider——这可能是我们不希望所发生的。

这个问题的解决方案是创建一个单独的provider,Configuration 中暴露你需要的内容(所以是 host):

避免听整个对象:

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
// 如果配置(configurations)发生变化,将导致productsProvider重新获取产品
final configs = await ref.watch(configProvider.future);

return dio.get('${configs.host}/products');
});

当你只需要一个对象的单个属性时,最好使用select:

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
// 只监听host。如果配置(configurations)中的其他内容发生了更改,这将重新评估我们的provider。
final host = await ref.watch(configProvider.selectAsync((config) => config.host));

return dio.get('$host/products');
});

这将只在 host 更改时重新构建productsProvider。