Why We Need AsyncValue of Riverpod

Why We Need AsyncValue of Riverpod

How AsyncValue Solves Issues on Asynchronous State Management

AsyncValue is one of the most essential objects for managing states built by asynchronous operation in Riverpod.

As most states are often built with asynchronous operations such as sending requests to web APIs or fetching from internal storage, we never see applications without using AsyncValue for their state management.

Looking at the very first page of the Riverpod document shows the simple example code including an API request, Remi, the author of the Riverpod package, would also think asynchronicity is inevitable for state management.

@riverpod
Future<String> boredSuggestion(BoredSuggestionRef ref) async {
  final response = await http.get(
    Uri.https('https://boredapi.com/api/activity'),
  );
  final json = jsonDecode(response.body) as Map;
  return json['activity']! as String;
}

However, it's difficult to completely understand what kind of data will be preserved by AsyncValue object and how we can properly handle it with confidence.

In this article, we are going to take a closer look at the structure of AsyncValue class first, discuss general concerns about state management with asynchronous operation next, and end up with how Riverpod helps us solve the concerns.

the Structure of AsyncValue

Let's go ahead and watch the AsyncValue class first.

AsyncValue and 3 Subclasses

First of all, AsyncValue has three subclasses AsyncLoading, AsyncData, and AsyncError.

Each class represents the state of asynchronous operations like below.

  • AsyncLoading represents that the operation is being performed.

  • AsyncData represents that the operation is done without errors.

  • AsyncError represents that the operation ended with an error.

Looking at it, some would say "OK, then, these three classes represent the state of asynchronous operations perfectly!", but that's not the case. They DON'T sufficiently represent all the states yet.

Additional Fields of AsyncValue

AsyncValue and its extension AsyncValueX have some fields and getters. Some of them are listed below.

FieldPurpose
valuepreserves data built with asynchronous operation
hasValueexposes if the value is already built
isLoadingexposes if the process is in a loading state
hasErrorexposes if an error in the last build
errorpreserves an error caused in the asynchronous operation
isRefreshingexposes if the process is in a "refreshing" state
isReloadingexposes if the process is in a "reloading" state

As AsyncValue is a superclass of AsyncLoading, AsyncData, and AsyncError, those three subclasses also have the same fields and getters.

But it's curious, isn't it?

Even though the state of asynchronous operations can be represented with the type of subclasses, why do they need additional fields, such as isLoading, which seems to represent the same information as the type AsyncLoading.

It's because the thing is not that simple.

Before talking more about AsyncValue, let's head to the next discussion; the typical issues about state management with asynchronous operations.

Typical Issues about State Management with Asynchronicity

In typical situations, the state of asynchronous operation starts with loading state that represents that the operation is "in processing"; waiting for a response from the server for example.

A Gif demonstrates operation is in the loading state

Once the operation completes successfully, UI can be built using the fetched data.

On the other hand, if some errors occur in the operation, UI would catch the error and show users something happened internally (and maybe say "try again later").

The image below summarizes the flow explained so far.

Unfortunately, however, this flow doesn't cover all the states we have to consider.

Once the operation completes and the data is visible on the screen, the application may provide a chance to refresh the state to let the user check the newest information.

The problem is that the state for refreshing is completely different from the "loading" state because the application may want to keep the data visible during refreshing and may NOT want the center progress indicator visible.

Thus, we need to distinguish the first "loading" state and the second "refreshing" state somehow, and the flow should be updated below including the "refreshing" state.

As we can see in the image above, an "error" can also happen after refreshing and the number of patterns increases twice.

How can we manage that complex flow? Do we need to define bool isLoading, T value, bool isRefreshing, and other relevant flags in all the widgets? We now want a handy class that can represent every single state above, don't we?

That's the AsyncValue.

How AsyncValue Solve the Issue

As we saw above, AsyncValue represents the state of asynchronous operation with its type and fields.

The image below explains how AsyncValue (and its subclasses) represents the flow explained so far.

It's quite simple the first time. AsyncLoading represents the loading state and transforms to AsyncData when the operation is completed and to AsyncError if some errors happen.

The trick is the second time. AsyncValue represents a "refreshing" state with the combination of the field of hasValue: true and isLoading: true; so as hasError: true.

We can see that in the implementation of isRefreshing getter below.

bool get isRefreshing =>
      isLoading && (hasValue || hasError) && this is! AsyncLoading;

In addition, we can see that the actual type of AsyncValue is not AsyncLoading in the refreshing state, it's AsyncData or AsyncError.

Reloading State

AsyncValue has an additional state "reloading" which is different from the "refreshing" state, represented by the flag isReloading.

"Reloading" state is not the same as "Refreshing" in terms of whether the condition is the same as the previous operation or not; re-fetching data with changing the filter conditions, for example.

isReloading is represented by the combination of the actual type and the hasValue / hasError field.

  bool get isReloading => (hasValue || hasError) && this is AsyncLoading;

In this case, isLoading flag is ignored because the flag is always true when the actual type is AsyncLoading.

class AsyncLoading<T> extends AsyncValue<T> {
  ...
  @override
  bool get isLoading => true;
}

Business of FutureProvider and AsyncNotifierProvider

So far, we have revealed the structure of AsyncValue and why we need such a complex structure to represent the flow of asynchronous operations.

The last question is "Who creates the AsyncValue objects properly?".

By using ref.watch(someAsyncProvider), we can obtain AsyncValue object that properly represents the current state of the asynchronous operation and properly preserves previously fetched data (or caused error). It means someone maintains the AsyncValue object somewhere and properly notifies its user, our ConsumerWidget for example.

That's the business of FutureProvider and AsyncNotifierProvider (or their associated ProviderElement internally).

As long as we use those providers (not StateNotifierProvider or other providers, and create AsyncValue objects manually), we receive the appropriate AsyncValue object at appropriate timings by ref.watch() or ref.listen().

.when()

Furthermore, .when() also helps us switch our UI depending on the state of the operation.

Typically, we may want to keep the previously fetched data visible in the "refreshing" state, so .when() calls a function passed to data argument on the "refreshing" state.

On the other hand, we may NOT want to keep the previously fetched data visible in the "reloading" state, because users may need other data fetched with other conditions meaning previous data is not necessary anymore, so .when() calls a function passed to loading argument on the "reloading" state by default.

Note that we can change the behavior by changing skipLoadingOnRefreshing and skipLoadingOnReloading flags passed to .when().


Conclusion

State management including asynchronicity is complex.

We need to maintain not only "loading", "completed", and "error" states but also "refreshing" states and following "completed again" or "error again" states.

AsyncValue and its maintainer FutureProvider / AsyncNotifierProvider provide us with a handy tool to manage these complex situations.

In addition, riverpod_generator will also boost the solution, which allows us to choose suitable providers only by changing the return type T into Future<T>.

Even if we are in some projects that don't use Riverpod, AsyncValue definitely provides us with ideas on how to solve the typical issues of asynchronicity.


If you want to watch the behavior of AsyncValue more, feel free to folk my practice project below. This will help you understand how the states will transact when .refresh() .invalidate() is called or the state is updated with state = newAsyncValue in a notifier class.

https://github.com/chooyan-eng/asyncvalue_practice