async_phase_notifier 0.8.0
async_phase_notifier: ^0.8.0 copied to clipboard
A variant of ValueNotifier that helps you better manage phases of asynchronous operations.
A ValueNotifier that has AsyncPhase representing the phases of
an asynchronous operation: initial, waiting, complete, and error.
AsyncPhaseNotifier 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
Basic Concept: AsyncPhase #
This package is built on top of async_phase.
AsyncPhase is a type that represents the current status of an asynchronous
operation. It consists of the following four states:
- AsyncInitial:
- The state before the operation has started.
- AsyncWaiting:
- The state where the operation is in progress. It can optionally hold the previous data.
- AsyncComplete:
- The state where the operation finished successfully. It contains the resulting data.
- AsyncError:
- The state where the operation failed. It contains an error and a stack trace, and can also hold the previous data.
By using AsyncPhaseNotifier, you can easily manage these transitions and reflect
them in your UI.
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.
Usage: AsyncPhaseNotifier #
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
AsyncWaitingwhen the operation starts. - The change is notified to listeners.
- The value is switched to either
AsyncCompleteorAsyncErrordepending on the result. - The change is notified to listeners.
final notifier = AsyncPhaseNotifier(0);
await notifier.update(() => someAsyncOperation());
Optionally, you can pass a callback function that takes the result of the operation
as a parameter. The onError callback is especially useful for logging errors.
await notifier.update(
() => someAsyncOperation(),
onWaiting: (data) => ...,
onComplete: (data) => ...,
onError: (e, s) => Logger.reportError(e, s),
);
Caution
Errors occurring in those callback functions are not automatically handled.
updateType() #
The updateType() method is used to update the phase type while preserving
the existing value.data.
- Preserves Existing Data:
- The callback result can be of any type and does not affect the notifier's current data.
- Direct Phase Mapping:
- If the callback returns an
AsyncPhase, the notifier adopts that specific phase. This is ideal when calling repository methods that already returnAsyncPhase(e.g., returningAsyncErrorwill transition the notifier to an error state without needing to manually throw an exception).
- If the callback returns an
- Flexible Implementation:
- Perfect for implementing command-like approaches where UI logic and business logic are decoupled.
For more technical details and behavior regarding callbacks, please refer to the updateType() documentation.
value.data vs data #
The data getter returns value.data. Using data is handier and more
type-safe since the return type is non-nullable if the generic type T of
AsyncPhaseNotifier<T> is non-nullable, whereas value.data 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
Streaminternally.
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.
final notifier = AsyncPhaseNotifier(Auth());
final cancel = notifier.listenFor(
onWaiting: (data) { /* e.g. Start loading indicator */ },
onComplete: (data) { /* e.g. Stop loading indicator */ },
onError: (e, s) { /* e.g. Showing an error dialog */ },
);
...
// Remove the listener if it is no longer necessary.
cancel();
Note
All callbacks are optional. Listener is not added if no callback function is passed.
AsyncPhaseListener
It is also possible to use the AsyncPhaseListener widget to listen for phase changes.
child: AsyncPhaseListener(
notifier: notifier,
onWaiting: (data) { /* e.g. Start loading indicator */ },
onComplete: (data) { /* e.g. Stop loading indicator */ },
onError: (e, s) { /* e.g. Showing an error dialog */ },
child: ...,
)
Caution
A listener is added for each AsyncPhaseListener widget, not per notifier.
If this widget is used in multiple places for the same notifier, a single
notification will trigger the callback function in each of those widgets.
Caution
Changes to callbacks (e.g. onComplete) will only take effect when the widget
is provided with a new key or a new notifier.
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 loading 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 #
- ❌ Add API documents
- ✅
Write tests