Re-walking the road of Flutter state management - the final chapter of Riverpod

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.

final myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0);
final myFamilyProvider =<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.

final userProvider =<User, int>((ref, userId) async {
  return fetchUser(userId);


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.

final messagesFamily =<Message, String>((ref, id) async {
  return dio.get('$id');

The syntax is slightly different when using our messagesFamily Provider.

The usual syntax like below will no longer work.

Widget build(BuildContext context, WidgetRef ref) {
  // Error – messagesFamily is not a provider
  final response =;

Instead, we need to pass a parameter to messagesFamily .

Widget build(BuildContext context, WidgetRef ref) {
  final response ='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.

Widget build(BuildContext context, WidgetRef ref) {
final frenchTitle = Locale('fr')));
final englishTitle = 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.

final characters =<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,
  • Objects generated with Freezed or build_value,
  • Using equatable objects,

Below is an example of using Freezed or equatable for multiple parameters.

abstract class MyParameter with _$MyParameter {
  factory MyParameter({
    required int userId,
    required Locale locale,
  }) = _MyParameter;

final exampleProvider =<Something, MyParameter>((ref, myParameter) {
  // Do something with userId/locale

Widget build(BuildContext context, WidgetRef ref) {
  int userId; // Read the user ID from somewhere
  final locale = Localizations.localeOf(context);

  final something =
    exampleProvider(MyParameter(userId: userId, locale: locale)),



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.

final 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.

final userProvider =<User, String>((ref, id) {



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.

final myProvider = FutureProvider.autoDispose((ref) async {
  final response = await httpClient.get(...);
  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.

final 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
  return response;


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.

final 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>';

This is not desirable as this will cause firstProvider to never be dispose.

To solve this problem, consider marking secondProvider with .autoDispose.

final firstProvider = Provider.autoDispose((ref) => 0);

final secondProvider = Provider.autoDispose((ref) {;

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.

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

We can now create another Provider which will consume our cityProvider.

final weatherProvider = FutureProvider((ref) async {
  // We use `` to listen to another provider, and we pass it the provider
  // that we want to consume. Here: cityProvider
  final city =;

  // 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. ❞


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.

class 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.
enum Filter {

final filterProvider = StateProvider((ref) => Filter.none);
  • Make a separate Provider, combine the filtering method with the todo-list, and expose the filtered todo-list.
final filteredTodoListProvider = Provider<List<Todo>>((ref) {
  final filter =;
  final todos =;

  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.

// 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 =;
  final configs = await;
  final response = await dio.get('${}/characters?search=$search');

  return => 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 to the created object. The created object will then be able to read the Provider at any time.

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

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

class Repository {

  /// The `` function
  final Reader read;

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

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

    return Catalog.fromJson(;

You can also pass ref instead of to your object.

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

class Repository {

  final Ref ref;

The only difference passing in is that it is slightly less verbose and ensures that our objects never use . ❞

However, never do it like below.

final myProvider = Provider((ref) {
  // Bad practice to call `read` here
  final value =;

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.

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

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

  Repository repository =;

  await expectLater(

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.

final 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;

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

select should be used when you only need a single property of an object.

final 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 =>;

  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.

Posted by phpMitch on Sat, 03 Sep 2022 06:33:58 +0530