Essentials

Async Values

Handle asynchronous operations with FutureShard and StreamShard.

Overview

Shard provides specialized classes for handling asynchronous operations:

  • FutureShard<T> - For one-time async operations (API calls, database queries)
  • StreamShard<T> - For continuous data streams (WebSockets, real-time updates)
  • AsyncValue<T> - Sealed class representing loading, data, or error states
  • AsyncShardBuilder - Widget for building UI based on async state

AsyncValue

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

sealed class AsyncValue<T> {
  // States
  AsyncLoading<T>   // Operation in progress
  AsyncData<T>      // Completed with data
  AsyncError<T>     // Failed with error
}

Properties

final asyncValue = AsyncData<int>(42);

asyncValue.isLoading    // false
asyncValue.hasData      // true
asyncValue.hasError     // false
asyncValue.dataOrNull   // 42
asyncValue.errorOrNull  // null

Pattern Matching

Use Dart's pattern matching for clean state handling:

final widget = switch (asyncValue) {
  AsyncLoading() => CircularProgressIndicator(),
  AsyncData(:final data) => Text('Value: $data'),
  AsyncError(:final error) => Text('Error: $error'),
};

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, previousData: currentUser)

FutureShard

FutureShard<T> manages state from a Future. It automatically handles loading, success, and error states.

Creating a FutureShard

class UserShard extends FutureShard<User> {
  final String userId;
  final UserRepository repository;

  UserShard({required this.userId, required this.repository});

  @override
  Future<User> build() async {
    return await repository.getUser(userId);
  }
}

Refreshing Data

Call refresh() to re-execute the build() method:

context.read<UserShard>().refresh();

During refresh:

  • State transitions to AsyncLoading with previousData preserved
  • UI can show stale data while loading new data
  • On success/error, state updates accordingly

Error Handling

Errors are automatically caught and reported:

class DataShard extends FutureShard<Data> {
  @override
  Future<Data> build() async {
    // If this throws, state becomes AsyncError
    return await api.fetchData();
  }

  @override
  void onError(Object error, StackTrace? stackTrace) {
    // Optionally handle errors locally
    print('Failed to fetch data: $error');
    super.onError(error, stackTrace);
  }
}

StreamShard

StreamShard<T> manages state from a Stream. Perfect for real-time data like WebSocket connections, database listeners, or timers.

Creating a StreamShard

class MessagesStream extends StreamShard<List<Message>> {
  final String chatId;
  final ChatRepository repository;

  MessagesStream({required this.chatId, required this.repository});

  @override
  Stream<List<Message>> build() {
    return repository.watchMessages(chatId);
  }
}

Refreshing Stream

Call refresh() to cancel current subscription and re-subscribe:

// Reset the stream
context.read<TimerShard>().refresh();

AsyncShardBuilder

AsyncShardBuilder simplifies building UI for async states with separate callbacks.

Basic Usage

AsyncShardBuilder<UserShard, User>(
  onLoading: (context) => CircularProgressIndicator(),
  onData: (context, user) => Text('Hello, ${user.name}'),
  onError: (context, error, stackTrace) => Text('Error: $error'),
)

Default Widgets

onLoading and onError are optional with sensible defaults:

// Uses default loading indicator and error display
AsyncShardBuilder<PostsShard, List<Post>>(
  onData: (context, posts) => PostList(posts: posts),
)

Showing Stale Data During Refresh

By default, showDataOnLoading is true, showing previous data while refreshing. Set to false to always show loading indicator:

AsyncShardBuilder<UserShard, User>(
  showDataOnLoading: true,
  onLoading: (context) => LinearProgressIndicator(),
  onData: (context, user) => UserCard(user: user),
)

Direct Shard Reference

final userShard = UserShard(userId: '123');

AsyncShardBuilder<UserShard, User>(
  shard: userShard,
  onData: (context, user) => UserProfile(user: user),
)

Using ShardBuilder with Pattern Matching

You can also use ShardBuilder with AsyncValue and pattern matching for more control:

ShardBuilder<UserShard, AsyncValue<User>>(
  builder: (context, asyncValue) {
    return switch (asyncValue) {
      AsyncLoading() => CircularProgressIndicator(),
      AsyncData(:final value) => Text('Hello, ${value.name}'),
      AsyncError(:final error) => Text('Error: $error'),
    };
  },
)

This approach gives you full control over the UI and allows you to use Dart's pattern matching for clean state handling.