In the last article, after we have mastered how to read the state value, and know how to choose different types of Providers according to different scenarios, and how to use Providers together, let’s take a look at some of its other features and see how they are Help us to better manage state.
Provider Modifiers
All providers have a built-in method to add extra functionality to your different providers.
They can add new functionality to the ref object, or slightly change the way the Provider consume s. Modifiers can be used on all providers, with a syntax similar to named constructors.
copyfinal myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0); final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');
Currently, there are two Modifiers available.
- .autoDispose, which will cause the Provider to automatically destroy its state when it is no longer being listened to
- .family, which allows to create a Provider with one external parameter
A Provider can use multiple Modifiers at the same time.
copyfinal userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async { return fetchUser(userId); });
.family
The .family modifier has one purpose: to create a unique Provider based on external parameters. Some common use cases for families are the following.
- Combine FutureProvider with .family to get a Message object from its ID
- Pass the current Locale to the Provider so we can handle the internationalization
The way family works is by adding an extra parameter to the Provider. This parameter can then be used freely in our Provider to create some state.
For example, we can combine family with FutureProvider to get a Message from its ID.
copyfinal messagesFamily = FutureProvider.family<Message, String>((ref, id) async { return dio.get('http://my_api.dev/messages/$id'); });
The syntax is slightly different when using our messagesFamily Provider.
The usual syntax like below will no longer work.
copyWidget build(BuildContext context, WidgetRef ref) { // Error – messagesFamily is not a provider final response = ref.watch(messagesFamily); }
Instead, we need to pass a parameter to messagesFamily .
copyWidget build(BuildContext context, WidgetRef ref) { final response = ref.watch(messagesFamily('id')); }
❝
We can use a variable with different parameters at the same time.
For example, we can use titleFamily to read both French and English translations.
copy@override Widget build(BuildContext context, WidgetRef ref) { final frenchTitle = ref.watch(titleFamily(const Locale('fr'))); final englishTitle = ref.watch(titleFamily(const Locale('en'))); return Text('fr: $frenchTitle en: $englishTitle'); }
❞
parameter limit
In order for families to work correctly, the parameters passed to the Provider must have a consistent hashCode and ==.
Ideally, the parameter should be a primitive type (bool/int/double/String), a constant (Provider), or an immutable object that overrides == and hashCode.
❝ Prefer to use autoDispose when the parameter is not constant ❞
You may want to use family to pass an input to the search field to your Provider. But this value may change frequently and will never be reused. This can lead to memory leaks because, by default, Provider s are not destroyed even if they are no longer used.
This memory leak can be fixed by using both .family and .autoDispose.
copyfinal characters = FutureProvider.autoDispose.family<List<Character>, String>((ref, filter) async { return fetchCharacters(filter: filter); });
Pass multiple arguments to family
family has no built-in support for passing multiple values to a Provider. On the other hand, this value can be anything (as long as it conforms to the aforementioned constraints).
This includes the following types.
- tuple type, Python-like tuple, https://pub.dev/packages/tuple
- Objects generated with Freezed or build_value, https://pub.dev/packages/freezed
- Using equatable objects, https://pub.dev/packages/equatable
Below is an example of using Freezed or equatable for multiple parameters.
copy@freezed abstract class MyParameter with _$MyParameter { factory MyParameter({ required int userId, required Locale locale, }) = _MyParameter; } final exampleProvider = Provider.autoDispose.family<Something, MyParameter>((ref, myParameter) { print(myParameter.userId); print(myParameter.locale); // Do something with userId/locale }); @override Widget build(BuildContext context, WidgetRef ref) { int userId; // Read the user ID from somewhere final locale = Localizations.localeOf(context); final something = ref.watch( exampleProvider(MyParameter(userId: userId, locale: locale)), ); ... }
.autoDispose
A common use case for this is to destroy a Provider's state when it is no longer in use.
There are many reasons for this, such as the following scenarios.
- When using Firebase, close connections and avoid unnecessary charges
- To reset the state when the user leaves a screen and re-enters
Provider has built-in support for this use case via .autoDisposeModifiers.
To tell Riverpod to destroy a Provider's state when it is no longer in use, simply attach .autoDispose to your Provider.
copyfinal userProvider = StreamProvider.autoDispose<User>((ref) { });
That's all. Now the state of the userProvider will be automatically destroyed when it is no longer used.
Note how the generic parameter is passed after autoDispose instead of before -- autoDispose is not a named constructor.
You can combine .autoDispose with other Modifiers if needed.
copyfinal userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) { });
ref.keepAlive
When marking a Provider with autoDispose, an additional method is also added to the ref: keepAlive.
The keep function is used to tell Riverpod that the state of the Provider should be preserved even if it is no longer being monitored.
One use case for it is to set this flag to true after an HTTP request has completed.
copyfinal myProvider = FutureProvider.autoDispose((ref) async { final response = await httpClient.get(...); ref.keepAlive(); return response; });
This way, if the request fails and the UI leaves the screen and then re-enters the screen, the request will be executed again. But if the request completes successfully, the state will be preserved and re-entering the screen will not trigger a new request.
Example: Automatically cancel when Http request is no longer used
autoDisposeModifiers can be combined with FutureProvider and ref.onDispose to easily cancel HTTP requests when they are no longer needed.
our target is:
- Initiate an HTTP request when the user enters a screen
- Cancel the HTTP request if the user leaves the screen before the request is complete
- If the request is successful, leaving and re-entering the screen does not initiate a new request
In code, this would look like the following.
copyfinal myProvider = FutureProvider.autoDispose((ref) async { // An object from package:dio that allows cancelling http requests final cancelToken = CancelToken(); // When the provider is destroyed, cancel the http request ref.onDispose(() => cancelToken.cancel()); // Fetch our data and pass our `cancelToken` for cancellation to work final response = await dio.get('path', cancelToken: cancelToken); // If the request completed successfully, keep the state ref.keepAlive(); return response; });
abnormal
When using .autoDispose, you may find that your application fails to compile with an error similar to the one below.
❝The argument type 'AutoDisposeProvider' can't be assigned to the parameter type 'AlwaysAliveProviderBase' ❞
Don't worry! This error is normal. It happens because you most likely have a bug.
For example, you try to listen to a Provider marked .autoDispose in a Provider that is not marked .autoDispose, such as the following code.
copyfinal firstProvider = Provider.autoDispose((ref) => 0); final secondProvider = Provider((ref) { // The argument type 'AutoDisposeProvider<int>' can't be assigned to the // parameter type 'AlwaysAliveProviderBase<Object, Null>' ref.watch(firstProvider); });
This is not desirable as this will cause firstProvider to never be dispose.
To solve this problem, consider marking secondProvider with .autoDispose.
copyfinal firstProvider = Provider.autoDispose((ref) => 0); final secondProvider = Provider.autoDispose((ref) { ref.watch(firstProvider); });
provider state association and integration
We have seen how to create a simple Provider before. But the reality is that in many cases, one Provider will want to read the state of another Provider.
To do this, we can use the ref object passed to our Provider's callback, and use its watch method.
As an example, consider the following Provider.
copyfinal cityProvider = Provider((ref) => 'London');
We can now create another Provider which will consume our cityProvider.
copyfinal 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 have created a Provider that depends on another Provider.
❝ This is actually mentioned in the previous example, ref can be connected to multiple different Provider s, which is a very flexible embodiment of Riverpod. ❞
FAQ
What if the value being listened to changes over time?
Depending on the Provider you are listening on, the value obtained may change over time. For example, you may be listening to a StateNotifierProvider, or the listening Provider may have been forced to refresh by using ProviderContainer.refresh/ref.refresh.
When using watch, Riverpod can detect that the monitored value has changed, and will automatically re-execute the Provider creation callback when needed.
This is useful for computing state. For example, consider a StateNotifierProvider that exposes a todo-list.
copyclass TodoList extends StateNotifier<List<Todo>> { TodoList(): super(const []); } final todoListProvider = StateNotifierProvider((ref) => TodoList());
A common use case is to have the UI filter the list of todos to only show completed/unfinished todos.
A simple way to achieve this is.
- Create a StateProvider that exposes the currently selected filter method.
copyenum Filter { none, completed, uncompleted, } final filterProvider = StateProvider((ref) => Filter.none);
- Make a separate Provider, combine the filtering method with the todo-list, and expose the filtered todo-list.
copyfinal 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(); } });
Then, our UI can listen to filteredTodoListProvider to listen to the filtered todo-list. Using this approach, the UI will automatically update when the filter or todo-list changes.
To see this approach in action, you can take a look at the source code of the Todo List example.
❝
This behavior is not specific to providers, it applies to all providers.
For example, you can combine a watch with a FutureProvider to implement a search function that supports real-time configuration changes.
copy// 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.future); 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 refetch that list when the configuration changes or the search query changes. ❞
Can I read a provider without listening to it?
Sometimes, we want to read the contents of a Provider, but not need to recreate the value when the obtained value changes.
An example is a Repository that reads user token s from another Provider for authentication.
We could use observations and create a new Repository when the user token changes, but doing so is of little use.
In this case, we can use read, which is similar to listen, but does not cause the Provider to recreate its value when the obtained value changes.
A common practice in this case is to pass ref.read to the created object. The created object will then be able to read the Provider at any time.
copyfinal 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); final response = await dio.get('/path', queryParameters: { 'token': token, }); return Catalog.fromJson(response.data); } }
❝
You can also pass ref instead of ref.read to your object.
copyfinal repositoryProvider = Provider((ref) => Repository(ref)); class Repository { Repository(this.ref); final Ref ref; }
The only difference passing in ref.read is that it is slightly less verbose and ensures that our objects never use ref.watch . ❞
However, never do it like below.
copyfinal myProvider = Provider((ref) { // Bad practice to call `read` here final value = ref.read(anotherProvider); });
If you use read as an attempt to avoid too many refreshes and rebuilds, see the FAQ below
How to test an object that receives read as a parameter of its constructor?
If you're using the pattern described in Can I read a Provider without listening to it, you may be wondering how to write tests for your objects.
In this case, consider testing the Provider directly instead of the original object. You can do this by using the ProviderContainer class.
copyfinal repositoryProvider = Provider((ref) => Repository(ref.read)); test('fetches catalog', () async { final container = ProviderContainer(); addTearOff(container.dispose); Repository repository = container.read(repositoryProvider); await expectLater( repository.fetchCatalog(), completion(Catalog()), ); });
My provider updates too often, what can I do?
If your objects are being recreated too often, your Provider is likely listening for objects it doesn't care about.
For example, you might be listening on a configuration object, but only use the host property.
By listening to the entire configuration object, this will still cause your Provider to be re-evaluated if properties other than host change - which may not be desirable.
The solution to this problem is to create a separate Provider that only exposes what you need in the config (hence the host).
Listening to the entire object should be avoided, as in the code below.
copyfinal configProvider = 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(configProvider.future); return dio.get('${configs.host}/products'); });
select should be used when you only need a single property of an object.
copyfinal configProvider = StreamProvider<Configuration>(...); 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(configProvider.selectAsync((config) => config.host)); return dio.get('$host/products'); });
This will only rebuild the productsProvider when the host changes.
Through these three articles, I believe that everyone has been able to use Riverpod proficiently. Compared with package:Provider, the use of Riverpod is simpler and more flexible. This is also a very important reason why I recommend it. After getting started, you can Learn according to the examples provided by the author in the document, and fully understand the usage skills of Riverpod in actual combat.
The original public account of this article: Qunying Chuan, please contact WeChat (Tomcat_xu) for authorized reprinting. After authorization, please reprint it 24 hours after the original publication.