Essentials

Async Values

Understand AsyncValue - the sealed class for idle, loading, data, and error states.

Overview

Shard provides specialized classes for handling asynchronous operations:

  • FutureShard - For one-time async operations (API calls, database queries)
  • StreamShard - For continuous data streams (WebSockets, real-time updates)
  • CommandShard - For mutation-style actions that start idle until executed
  • AsyncValue<T> - Sealed class representing idle, loading, data, or error states
  • AsyncShardBuilder - Widget for building UI based on async state (see Widgets)

AsyncValue

AsyncValue<T> is a sealed class that represents the state of an async operation:

sealed class AsyncValue<T> {
  // States
  AsyncIdle<T>      // Not started yet (e.g. a CommandShard before execute)
  AsyncLoading<T>   // Operation in progress
  AsyncData<T>      // Completed with data
  AsyncError<T>     // Failed with error
}

AsyncIdle is the not-yet-started state. FutureShard and StreamShard never enter it (they begin loading); it exists so mutation-style shards like CommandShard can start idle.

Properties

final asyncValue = AsyncData<int>(42);

asyncValue.isIdle       // false
asyncValue.isLoading    // false
asyncValue.hasData      // true
asyncValue.hasError     // false
asyncValue.dataOrNull   // 42 (AsyncLoading/AsyncError return previousData; AsyncIdle returns null)
asyncValue.errorOrNull  // null

Pattern Matching

Use Dart's pattern matching for clean state handling. Because AsyncValue is sealed with four cases, an exhaustive switch must cover all of them:

// Exhaustive switch — all four cases now required:
final widget = switch (asyncValue) {
  AsyncIdle() => const Text('Tap to load'),
  AsyncLoading() => const CircularProgressIndicator(),
  AsyncData(:final data) => Text('Value: $data'),
  AsyncError(:final error) => Text('Error: $error'),
};

For most code, the when method (recommended) is more concise than a switch, and maybeWhen lets you handle only the cases you care about:

final text = asyncValue.when(
  idle: () => 'Tap to load',
  loading: (previousData) => previousData == null ? 'Loading…' : 'Refreshing…',
  data: (value) => 'Value: $value',
  error: (error, stackTrace, previousData) => 'Failed: $error',
);

final canSubmit = asyncValue.maybeWhen(
  data: (_) => true,
  orElse: () => false,
);

The loading and error callbacks receive previousData so you can render stale content while a refresh is in flight.

Transforming

Use mapData to transform the success value while preserving the current variant, and whenData to read the success value only when one is present:

// Transform the success value, preserving the variant (idle stays idle, etc.):
final AsyncValue<String> label = userState.mapData((u) => u.name);

// Read the success value only when present, else null:
final int? nameLength = userState.whenData((u) => u.name.length);

mapData keeps the shape of AsyncLoading and AsyncError, mapping any previousData through the same transform.

AsyncValue.guard

AsyncValue.guard wraps a future in a try/catch and returns the outcome as an AsyncValue. Use it to populate a shard without writing your own error handling:

class UserShard extends Shard<AsyncValue<User>> {
  UserShard(this.repo) : super(const AsyncIdle());
  final UserRepository repo;

  Future<void> load() async {
    emit(AsyncLoading(previousData: state.dataOrNull));
    emit(await AsyncValue.guard(() => repo.fetch()));
  }
}

guard runs the future and captures the outcome as AsyncData on success or AsyncError (with the stack trace and an optional previousData) when it throws. It never throws, so the await always resolves to a valid state you can emit directly.

Previous Data

Both AsyncLoading and AsyncError can retain previous data:

// During refresh, previous data is preserved
AsyncLoading<User>(previousData: currentUser)

// On error, previous data is still accessible
AsyncError<User>(error, stackTrace, currentUser)

The AsyncError constructor takes the error first, then an optional stackTrace and optional previousData: AsyncError(error, [stackTrace, previousData]). Retaining previous data lets dataOrNull keep returning the last successful value while loading or after a failure, so the UI never has to flash empty.

Building UI

To build UI for async state, use AsyncShardBuilder (see Widgets) or ShardBuilder with AsyncValue and pattern matching. For the shard types themselves, see FutureShard and StreamShard.

Next Steps

Next: Response Caching to cache API responses and reduce network calls with FutureShard.

See also: CommandShard for mutation-style actions that start in the idle state.