Advanced waiting
It's a common task having to show a visual indication that some important process is taking some time to finish (and will hopefully finish soon). For example:
-
A save button that displays spinner (in Flutter, a
CircularProgressIndicator
) while some info is saving. -
A
Text("Please wait...")
that is displayed in the center of the screen while some info is being calculated. -
A shimmer that is displayed as a placeholder while some widget info is being downloaded.
-
A modal barrier that prevents the user from interacting with the screen while some info is loading or saving.
In Wait, fail, succeed I explain that the easiest way to do that is to use:
isWaiting(MyAction)
in actionscontext.isWaiting(MyAction)
in widgets
Where MyAction
is the async action you are waiting for.
This is extremely easy to use and works well for the majority of cases.
If you have a rare case where store.isWaiting()
is not enough, keep reading.
WaitAction and Wait
In Before and After the Reducer section I show
how to manually create a BarrierAction
with a boolean flag that is used to add or remove a modal
barrier in the screen (see the
code here).
Creating and managing a boolean flag yourself will work in some rare complex cases
where store.isWaiting()
is not enough.
However, keeping track of many such boolean flags may be difficult to do.
If you need help with this problem, an option is using the built-in classes WaitAction
and Wait
.
To use them, your store state must have a Wait
field named wait
, and then your state class
must have a copy
or a copyWith
method which copies this field as a named parameter. For example:
class AppState {
final Wait wait;
...
AppState({this.wait, ...});
AppState copy({Wait wait, ...}) => AppState(wait: wait ?? this.wait, ...);
}
Then, when you want to start waiting, simply dispatch a WaitAction
and pass it some immutable object to act as a flag. When you finish waiting, just remove the flag.
For example:
dispatch(WaitAction.add("my flag")); // To add a flag.
dispatch(WaitAction.remove("my flag")); // To remove a flag.
When you are using the state:
state.wait.isWaitingAny
returnstrue
if there's any waiting whatsoever.state.wait.isWaiting(flag)
returnstrue
if you are waiting for a specificflag
state.wait.isWaiting(flag, ref: reference)
returnstrue
if you are waiting for a specificreference
of theflag
.state.wait.isWaitingForType<T>()
returnstrue
if you are waiting for any flag of typeT
.
The flag can be any convenient immutable object, like a URL, a user id, an index, an enum, a String, a number, or other.
As an example, if we want to replace the store.isWaiting()
method with the Wait
object, we
could do this: Suppose that a button dispatches a LoadAction
to load some text. You can make the
button show a progress indicator while the text is being loaded, and show the text when it's done:
class LoadAction extends ReduxAction<AppState> {
Future<AppState> reduce() async {
var newText = await loadText();
return state.copy(text: newText);
}
void before() => dispatch(WaitAction.add(this));
void after() => dispatch(WaitAction.remove(this));
}
Then, in the button:
if (wait.isWaitingForType<LoadAction>()) { // Show the button as disabled }
else { // Show the button as enabled }
Note: You may also define a mixin to implement the waiting:
mixin WithWaitState implements ReduxAction<AppState> {
void before() => dispatch(WaitAction.add(this));
void after() => dispatch(WaitAction.remove(this));
}
class LoadAction extends ReduxAction<AppState> with WithWaitState {
Future<AppState> reduce() async {
var newText = await loadText();
return state.copy(text: newText);
}
}
Try running
the: Wait Action Simple Example
(which is similar to the
Before and After example
but using the built-in WaitAction
). It uses the action itself as the
flag, by passing this
.
A more advanced example is
the Wait Action Advanced 1 Example.
Here, 10 buttons are shown. When a button is clicked it will be
replaced by a downloaded text description. Each button shows a progress indicator while its
description is downloading. Also, the screen title shows the text "Downloading..."
if any of the
buttons is currently downloading.
The flag in this case is simply the index of the button, from 0
to 9
:
int index;
void before() => dispatch(WaitAction.add(index));
void after() => dispatch(WaitAction.remove(index));
In the ViewModel
, just as before, if there's any waiting, then state.wait.isWaitingAny
will
return true
. However, now you can check each button wait flag separately by its index.
state.wait.isWaiting(index)
will return true
if that specific button is waiting.
Note: If necessary, you can clear all flags by doing dispatch(WaitAction.clear())
.
If you fear your flag may conflict with others, you can also add a "namespace", by further dividing flags into references. This can be seen in the Wait Action Advanced 2 Example:
void before() => dispatch(WaitAction.add("button-download", ref: index));
void after() => dispatch(WaitAction.remove("button-download", ref: index));
Now, to check a button's wait flag, you must pass both the flag and the reference:
state.wait.isWaiting("button-download", ref: index)
.
Note: If necessary, you can clear all references of that flag by
doing dispatch(WaitAction.clear("button-download"))
.
You can also pass a delay to WaitAction.add()
and WaitAction.remove()
methods. Please refer to
their method documentation for more information.
Freezed package
How to use Freezed, BuiltValue, or other similar code generator packages?
In case you use
built_value
or freezed packages, the WaitAction
works
out-of-the-box with them. In both cases, you don't need to create the copy
or copyWith
methods
by hand. But you still need to add the Wait
object to the store state as previously described.
If you want to further customize WaitAction
to work with other packages, or to use the Wait
object in different ways, you can do so by injecting your custom reducer into WaitAction.reducer
during your app's initialization. Refer to
the WaitAction
documentation for more information.