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 statesAsyncShardBuilder - Widget for building UI based on async stateAsyncValue<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
}
final asyncValue = AsyncData<int>(42);
asyncValue.isLoading // false
asyncValue.hasData // true
asyncValue.hasError // false
asyncValue.dataOrNull // 42
asyncValue.errorOrNull // null
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'),
};
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<T> manages state from a Future. It automatically handles loading, success, and error states.
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);
}
}
Call refresh() to re-execute the build() method:
context.read<UserShard>().refresh();
During refresh:
AsyncLoading with previousData preservedErrors 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<T> manages state from a Stream. Perfect for real-time data like WebSocket connections, database listeners, or timers.
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);
}
}
Call refresh() to cancel current subscription and re-subscribe:
// Reset the stream
context.read<TimerShard>().refresh();
AsyncShardBuilder simplifies building UI for async states with separate callbacks.
AsyncShardBuilder<UserShard, User>(
onLoading: (context) => CircularProgressIndicator(),
onData: (context, user) => Text('Hello, ${user.name}'),
onError: (context, error, stackTrace) => Text('Error: $error'),
)
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),
)
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),
)
final userShard = UserShard(userId: '123');
AsyncShardBuilder<UserShard, User>(
shard: userShard,
onData: (context, user) => UserProfile(user: user),
)
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.