Combining providers

Make sure to read about Providers first.
In this guide, we will see everything there is to know about combining providers.

Combining providers

We've previously seen how to create a simple provider. But the reality is, in many situation 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:

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

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

final weatherProvider = FutureProvider((ref) async {
// 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 listened changes over time?

Depending on the provider that you are listening, the value obtained may change over time.
For example, you may be listening to a StateNotifierProvider, or the provider listened was forced to refresh using ProviderContainer.refresh/context.refresh.

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

This can be useful for computed states. For example, consider a StateNotifierProvider that exposes a todo-list:

class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}
final todoListProvider = StateNotifierProvider((ref) => TodoList());

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 scenario would be to:

  • create 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:

    final filteredTodoListProvider = Provider<List<Todo>>((ref) {
    final filter = ref.watch(filterProvider);
    final todos = ref.watch(todoListProvider.state);
    switch (filter.state) {
    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 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

info

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 or live-configurations change:

// The current search filter
final searchProvider = StateProvider((ref) => '');
/// Configurations which can change over time
final configsProvider = StreamProvider<Configuration>(...);
final charactersProvider = FutureProvider<List<Character>>((ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configsProvider.last);
final response = await dio.get('${configs.host}/characters?search=$search');
return response.data.map((json) => Character.fromJson(json)).toList();
});

This code will fetch a list of characters from the service, and automatically re-fetch the list whenever the configurations changes 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 authentification.
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 its value exposed when the value obtained changes.

In that case, a common practice is to pass ref.read 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((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
/// The `ref.read` function
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider).state;
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
note

You could also pass the ref instead of ref.read to your object:

final repositoryProvider = Provider((ref) => Repository(ref));
class Repository {
Repository(this.ref);
final ProviderReference ref;
}

The only difference that passing ref.read brings is a slightly less verbose code and making sure that our object never uses ref.watch.

DON'T call read inside the body of a provider
final myProvider = Provider((ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
});

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 read as 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.read));
test('fetches catalog', () async {
final container = ProviderContainer();
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 chances are, your provider is listening to objects that it doesn't care about.

For example, you may be listening a Configuration object, but only use the host property.
By listening to the entire Configuration object, if a property other than host changes, this would still cause 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:

final configsProvider = StreamProvider<Configuration>(...);
final productsProvider = FutureProvider<List<Product>>((ref) async {
// Will cause productsProvider to re-fetch the products if anything in the
// configurations changes
final configs = await ref.watch(configsProvider.last);
return dio.get('${configs.host}/products');
});

PREFER listening to only what you use:

final configsProvider = StreamProvider<Configuration>(...);
/// A provider that exposes only the current host
final _hostProvider = Provider<AsyncValue<String>>((ref) {
return ref.watch(configsProvider).whenData((configs) => configs.host);
});
final productsProvider = FutureProvider<List<Product>>((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(_hostProvider);
return dio.get('$host/products');
});