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.
Field | Purpose |
value | preserves data built with asynchronous operation |
hasValue | exposes if the value is already built |
isLoading | exposes if the process is in a loading state |
hasError | exposes if an error in the last build |
error | preserves an error caused in the asynchronous operation |
isRefreshing | exposes if the process is in a "refreshing" state |
isReloading | exposes 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.
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.