Pub Version async_phase CI codecov

A sealed class and its subclasses representing phases of an asynchronous operation.

About this package

This package is mainly for use with AsyncPhaseNotifier in Flutter apps, but has been made public as a separate package so that it can be used for pure Dart apps too.

For details on AsyncPhaseNotifier, see its document.

AsyncPhase

AsyncPhase is similar to AsyncValue, which is part of package:riverpod. Unlike it, this AsyncPhase is an independent package, so you can use it without unnecessary dependencies, and is much simpler without surprising behaviours.

Subclasses (Phases)

AsyncPhase itself is a sealed class. Its four subclasses listed below are used to represent phases of an asynchronous operation.

Properties

  • data
    • The result of an asynchronous operation.
    • Nullable basically, but non-nullable in AsyncComplete<T> if the T is non-nullable.
  • error
    • The error that occurred in an asynchronous operation.
    • This property only exists in AsyncError.
  • stackTrace
    • The stack trace of the error that occurred in an asynchronous operation.
    • This property only exists in AsyncError.

Usage

This section explains usages without AsyncPhaseNotifier.

For use with AsyncPhaseNotifier, see the document of async_phase_notifier.

AsyncPhase.from()

Use AsyncPhase.from() to execute an asynchronous function and transform the result into either an AsyncComplete or an AsyncError.

  1. Use AsyncInitial first.
  2. Switch it to AsyncWaiting when an asynchronous operation starts.
  3. Use AsyncPhase.from() to run the operation.
  4. The result of the operation is returned; either AsyncComplete or AsyncError.

Example

class WeatherForecast {
  WeatherForecast({required this.onPhaseChanged});

  final void Function(AsyncPhase<Weather>) onPhaseChanged;

  AsyncPhase<Weather> _phase = AsyncInitial(Weather());

  Future<void> fetch() async {
    _phase = _phase.copyAsWaiting();
    onPhaseChanged(_phase);

    _phase = await AsyncPhase.from(
      () => repository.fetchWeather(Cities.tokyo),
      fallbackData: _phase.data,
    );
    onPhaseChanged(_phase);
  }
}

copyAsWaiting() is a handy method to switch the phase to AsyncWaiting without losing the previous data.

fallbackData is an argument for specifying the data that should be used when the asynchronous operation results in failure. If it is not specified, the data field of the resulting AsyncError is set to null.

when()

The when() method is useful for returning something that corresponds to the current phase, like a message, or a widget in a Flutter app.

If initial is not specified and the current phase is AsyncInitial, the callback function passed to waiting is called instead.

final message = phase.when(
  initial: (data) => 'phase: AsyncInitial ($data)', // Optional
  waiting: (data) => 'phase: AsyncWaiting ($data)',
  complete: (data) => 'phase: AsyncComplete ($data)',
  error: (data, error, stackTrace) => 'phase: AsyncError ($error)',
);

Pattern matching as an alternative to when()

Since AsyncPhase is a sealed class, it is possible to use pattern matching instead of when() to handle the phases exhaustively. Which to use is just a matter of preference.

final message = switch (phase) {
  AsyncInitial(:final data) => 'phase: AsyncInitial ($data)',
  AsyncWaiting(:final data) => 'phase: AsyncWaiting ($data)',
  AsyncComplete(:final data) => 'phase: AsyncComplete ($data)',
  AsyncError(:final error) => 'phase: AsyncError ($error)',
};

whenOrNull()

when() requires all parameters except for initial. If you need only some of them, use whenOrNull() instead.

Please note that null is returned as the name suggests if the current phase does not match any of the specified parameter.

e.g. In the example below, the result is null if the current phase is AsyncInitial or AsyncWaiting because initial and waiting have been omitted.

final message = phase.whenOrNull(
  complete: (data) => 'phase: AsyncComplete ($data)',
  error: (data, error, stackTrace) => 'phase: AsyncError ($error)',
);

Type checks

For checking if the current phase matches one of the four phases, you can use a getter; isInitial, isWaiting, isComplete or isError.

final phase = await AsyncPhase.from(...);

if (phase.isError) {
  return;
}

Using isError as shown above does not promote the type of the phase to AsyncError. To make error and stackTrace available, check the type with the is operator to get the flow analysis to work, or use pattern matching instead.

if (phase is AsyncError<Weather>) {
  print(phase.error);
  return;
}

or

if (phase case AsyncError(:final error)) {
  print(error);
  return;
}

rethrowError()

AsyncError has the rethrowError() method. It rethrows the error the AsyncError has with associated stack trace.

Future<AsyncPhase<Uint8List>> fetchImage({required Uri uri}) async {
  return AsyncPhase.from(() {
    final phase = await downloadFrom(uri: uri);
    if (phase case AsyncError()) {
      phase.rethrowError();
    }
    return resizeImage(phase.data, maxSize: ...);
  });
}

onComplete / onError

onComplete and onError of AsyncPhase.from() are handy if you just want to do something depending on whether an operation was successful.

final phase = await AsyncPhase.from(
  () => someOperation(),
  onComplete: (data) {
    // Called when the operation completes successfully.
  },
  onError: (data, error, stackTrace) {
    // Called when the operation fails.
  },
);

Libraries

async_phase