跳到主要内容

Combining Provider States

警告

本页内容可能已经过时。
今后会进行更新,但目前您可能需要参考侧边栏顶部的内容(介绍/要点/应用案例/......)。

Make sure to read Providers first.
In this guide, we will learn about combining provider states.

Combining provider states

We've previously seen how to create a simple provider. But the reality is, in many situations a provider will want to read the state of another provider.

To do that, we can use the ref object passed to the callback of our provider, and use its watch method.

As an example, consider the following provider:


String city(Ref ref) => 'London';

We can now create another provider that will consume our cityProvider:


Future<Weather> weather(Ref ref) {
// We use `ref.watch` to listen to another provider, and we pass it the provider
// that we want to consume. Here: cityProvider
final city = ref.watch(cityProvider);

// We can then use the result to do something based on the value of `cityProvider`.
return fetchWeather(city: city);
}

That's it. We've created a provider that depends on another provider.

FAQ

What if the value being listened to changes over time?

Depending on the provider that you are listening to, the value obtained may change over time. For example, you may be listening to a NotifierProvider, or the provider being listened to may have been forced to refresh through the use of ProviderContainer.refresh/ref.refresh.

When using watch, Riverpod is able to detect that the value being listened to changed and will automatically re-execute the provider's creation callback when needed.

This can be useful for computed states. For example, consider a (Async)NotifierProvider that exposes a todo-list:



class TodoList extends _$TodoList {

List<Todo> build() {
return [];
}
}

A common use-case would be to have the UI filter the list of todos to show only the completed/uncompleted todos.

An easy way to implement such a scenario would be to:

  • create a StateProvider, which exposes the currently selected filter method:

    enum Filter {
    none,
    completed,
    uncompleted,
    }

    final filterProvider = StateProvider((ref) => Filter.none);
  • make a separate provider which combines the filter method and the todo-list to expose the filtered todo-list:



    List<Todo> filteredTodoList(Ref 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();
    }
    }

Then, our UI can listen to filteredTodoListProvider to listen to the filtered todo-list.
Using such an approach, the UI will automatically update when either the filter or the todo-list changes.

To see this approach in action, you can look at the source code of the Todo List example.

信息

This behavior is not specific to Provider, and works with all providers.

For example, you could combine watch with FutureProvider to implement a search feature that supports live-configuration changes:


// The current search filter
final searchProvider = StateProvider((ref) => '');


Stream<Configuration> configs(Ref ref) {
return Stream.value(Configuration());
}


Future<List<Character>> characters(Ref ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configsProvider.future);
final response = await dio.get<List<Map<String, dynamic>>>(
'${configs.host}/characters?search=$search');

return response.data!.map(Character.fromJson).toList();
}

This code will fetch a list of characters from the service, and automatically re-fetch the list whenever the configurations change or when the search query changes.

Can I read a provider without listening to it?

Sometimes, we want to read the content of a provider, but without re-creating the value exposed when the value obtained changes.

An example would be a Repository, which reads from another provider the user token for authentication.
We could use watch and create a new Repository whenever the user token changes, but there is little to no use in doing that.

In this situation, we can use read, which is similar to watch, but will not cause the provider to recreate the value it exposes when the value obtained changes.

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.

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);
}
}
DON'T call read inside the body of a provider


MyValue my(Ref ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
return value;
}

If you used read as an attempt to avoid unwanted rebuilds of your object, refer to My provider updates too often, what can I do?

How to test an object that receives ref as a parameter of its constructor?

If you are using the pattern described in Can I read a provider without listening to it?, you may be wondering how to write tests for your object.

In this scenario, consider testing the provider directly instead of the raw object. You can do so by using the ProviderContainer class:

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()),
);
});

My provider updates too often, what can I do?

If your object is re-created too often your provider is likely listening to objects that it doesn't care about.

For example, you may be listening to a Configuration object, but only use the host property.
By listening to the entire Configuration object, if a property other than host changes, this still causes your provider to be re-evaluated – which may be undesired.

The solution to this problem is to create a separate provider that exposes only what you need in Configuration (so host):

AVOID listening to the entire object:



Stream<Configuration> config(Ref ref) => Stream.value(Configuration());


Future<List<Product>> products(Ref ref) async {
// Will cause productsProvider to re-fetch the products if anything in the
// configurations changes
final configs = await ref.watch(configProvider.future);

final result =
await dio.get<List<Map<String, dynamic>>>('${configs.host}/products');
return result.data!.map(Product.fromJson).toList();
}

PREFER using select when you only need a single property of an object:



Stream<Configuration> config(Ref ref) => Stream.value(Configuration());


Future<List<Product>> products(Ref ref) async {
// Listens only to the host. If something else in the configurations
// changes, this will not pointlessly re-evaluate our provider.
final host =
await ref.watch(configProvider.selectAsync((config) => config.host));

final result = await dio.get<List<Map<String, dynamic>>>('$host/products');

return result.data!.map(Product.fromJson).toList();
}

This will only rebuild the productsProvider when the host changes.