A variant of ValueNotifier
that has AsyncPhase representing the initial /
waiting / complete / error phases of an asynchronous operation.
AsyncPhaseNotifier
+ AsyncPhase
is similar to AsyncNotifier
+ AsyncValue
of Riverpod.
Unlike AsyncNotifier and AsyncValue, which are tied to package:riverpod,
AsyncPhaseNotifier
and AsyncPhase
have no such binding. The notifier can be
used as just a handy variant of ValueNotifier
with AsyncPhase
as its value
and convenient methods for manipulating the phases.
Sample apps
- Useless Facts - simple
- pub.dev explorer - advanced
Usage
update()
The update() method of AsyncPhaseNotifier executes
an asynchronous function, updates the value
of AsyncPhaseNotifier
automatically
according to the phase of the asynchronous operation, and notifies the listeners of
those changes.
- The value of the notifier is switched to
AsyncWaiting
when the operation starts. - The change is notified to listeners.
- The value is switched to either
AsyncComplete
orAsyncError
depending on the result. - The change is notified to listeners.
final notifier = AsyncPhaseNotifier(0);
notifier.update(() => someAsyncOperation());
updateOnlyPhase()
This is the same as update() except that updateOnlyPhase()
only update the phase itself without updating value.data
, whereas update()
updates both the phase and value.data
.
This method is useful when it is necessary to update the phase during execution but the callback result should not affect the data.
e.g. Indicating the waiting status on the UI or notifying the phase change to other parts of the code, with the existing data being kept unchanged.
AsyncPhase
The value of AsyncPhaseNotifier
is either AsyncInitial,
AsyncWaiting, AsyncComplete or AsyncError.
They are subtypes of AsyncPhase.
AsyncPhase
provides the when() and whenOrNull() methods,
which are useful for choosing an action based on the current phase, like returning
an appropriate widget.
child: phase.when(
initial: (data) => Text('phase: AsyncInitial($data)'), // Optional
waiting: (data) => Text('phase: AsyncWaiting($data)'),
complete: (data) => Text('phase: AsyncComplete($data)'),
error: (data, error, stackTrace) => Text('phase: AsyncError($data, $error)'),
)
async_phase
is a separate package, included in this package. See
its document for details not covered here.
value.data vs data
data
is a getter for value.data
. The former is handy and more type-safe since
the return type is non-nullable if the generic type T
of AsyncPhaseNotifier<T>
is non-nullable, whereas tha latter is always nullable, often requiring a null
check or a non-null assertion.
Listening for phase changes
listen()
With listen(), you can trigger some action when the phase or its data changes.
This is not much different from addListener()
, except for the following points:
- Returns a function to easily stop listening.
- Takes a listener function that receives the phase at the time of the call.
- The listener is called asynchronously because this method uses
Stream
internally.
final notifier = AsyncPhaseNotifier(Auth());
final cancel = notifier.listen((phase) { /* Some action */ });
...
// Remove the listener if it is no longer necessary.
cancel();
listenFor()
With listenFor(), you can trigger some action in one of the callbacks relevant to the latest phase when the phase or its data changes.
Note:
- All callbacks are optional.
- Listener is not added if no callback function is passed.
- The
onWaiting
callback is called when the phase has changed toAsyncWaiting
and also fromAsyncWaiting
. A boolean value is passed to the callback to indicate the start or end of an asynchronous operation.
final notifier = AsyncPhaseNotifier(Auth());
final cancel = notifier.listenFor(
onWaiting: (isWaiting) { /* e.g. Toggling an indicator */ },
onComplete: (data) { /* e.g. Logging the result of an operation */ },
onError: (e, s) { /* e.g. Showing an error dialog */ },
);
...
// Remove the listener if it is no longer necessary.
cancel();
AsyncPhaseListener
It is also possible to use the AsyncPhaseListener widget to listen for phase changes.
child: AsyncPhaseListener(
notifier: notifier,
onWaiting: (isWaiting) { /* e.g. Toggling an indicator */ },
onComplete: (data) { /* e.g. Logging the result of an operation */ },
onError: (e, s) { /* e.g. Showing an error dialog */ },
child: ...,
)
Please note that a listener is added per each AsyncPhaseListener
, not per
notifier. If this widget is used at various places for one certain notifier,
a single notification causes each of them to run its callback function.
Examples
Here is WeatherNotifier extending AsyncPhaseNotifier
. It fetches the weather
info of a city and notifies its listeners.
class WeatherNotifier extends AsyncPhaseNotifier<Weather> {
WeatherNotifier() : super(const Weather());
final repository = WeatherRepository();
void fetch() {
update(() => repository.fetchWeather(Cities.tokyo));
}
}
final notifier = WeatherNotifier();
notifier.fetch();
The examples below use this notifier and show a particular UI component corresponding to each phase of the fetch.
With ValueListenableBuilder
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<AsyncPhase<Weather>>(
valueListenable: notifier,
builder: (context, phase, _) {
// Shows a progress indicator while fetching and
// either the result or an error when finished.
return phase.when(
waiting: (weather) => const CircularProgressIndicator(),
complete: (weather) => Text('$weather'),
error: (weather, e, s) => Text('$e'),
);
},
);
}
Or you can use AnimatedBuilder
/ ListenableBuilder
in a similar way.
With Provider
ValueListenableProvider<AsyncPhase<Weather>>.value(
value: notifier,
child: MaterialApp(home: ...),
)
@override
Widget build(BuildContext context) {
final phase = context.watch<AsyncPhase<Weather>>();
return phase.when(
waiting: (weather) => const CircularProgressIndicator(),
complete: (weather) => Text('$weather'),
error: (weather, e, s) => Text('$e'),
);
}
With Grab
void main() {
runApp(
const Grab(child: App()),
);
}
@override
Widget build(BuildContext context) {
final phase = notifier.grab(context);
return phase.when(
waiting: (weather) => const CircularProgressIndicator(),
complete: (weather) => Text('$weather'),
error: (weather, e, s) => Text('$e'),
);
}
TODO
x
Write tests