Combining Provider States
Make sure to 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(CityRef ref) => 'London';
We can now create another provider that will consume our cityProvider
:
Future<Weather> weather(WeatherRef 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(FilteredTodoListRef 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(ConfigsRef ref) {
return Stream.value(Configuration());
}
Future<List<Character>> characters(CharactersRef 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);
}
}
MyValue my(MyRef 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(ConfigRef ref) => Stream.value(Configuration());
Future<List<Product>> products(ProductsRef 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(ConfigRef ref) => Stream.value(Configuration());
Future<List<Product>> products(ProductsRef 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.