bloc_event_status 2.0.0
bloc_event_status: ^2.0.0 copied to clipboard
Track the status of events in a bloc without updating the state.
BlocEventStatus #
Compose event status tracking into your BLoC state.
Installation #
dart pub add bloc_event_status
Overview #
bloc_event_status lets you track the status of individual event types (loading, success, failure, or any custom status) directly inside your BLoC state. The status for each event type is stored in an EventStatuses field on the state, so you can react to it using standard flutter_bloc widgets (BlocListener, BlocBuilder, BlocSelector) without any extra widgets or streams.
Getting Started #
Step 1: Define your status type #
The package is status-agnostic — you define what statuses mean in your app. A sealed class is a natural fit:
sealed class EventStatus {
const EventStatus();
}
class LoadingEventStatus extends EventStatus {
const LoadingEventStatus();
}
class SuccessEventStatus extends EventStatus {
const SuccessEventStatus();
}
class FailureEventStatus extends EventStatus {
const FailureEventStatus(this.error);
final Exception error;
}
An enum works just as well for simpler cases.
Step 2: Add EventStatuses to your state #
Add an EventStatuses<TEvent, TStatus> field to your state class. This is the only required change to your state.
class TodoState {
const TodoState({
required this.todos,
required this.eventStatuses,
});
const TodoState.initial()
: todos = const [],
eventStatuses = const EventStatuses();
final List<Todo> todos;
final EventStatuses<TodoEvent, EventStatus> eventStatuses;
TodoState copyWith({
List<Todo>? todos,
EventStatuses<TodoEvent, EventStatus>? eventStatuses,
}) {
return TodoState(
todos: todos ?? this.todos,
eventStatuses: eventStatuses ?? this.eventStatuses,
);
}
}
Optional: mix in EventStatusesMixin to add convenience accessors directly on your state. This lets you write state.statusOf<TodoLoadRequested>() instead of state.eventStatuses.statusOf<TodoLoadRequested>().
class TodoState with EventStatusesMixin<TodoEvent, EventStatus> {
// ... same as above ...
@override
final EventStatuses<TodoEvent, EventStatus> eventStatuses;
}
The examples below use the mixin variant.
Step 3: Emit statuses in the BLoC #
Call eventStatuses.update<EventType>(event, status) and emit the resulting state via copyWith:
class TodoBloc extends Bloc<TodoEvent, TodoState> {
TodoBloc() : super(const TodoState.initial()) {
on<TodoLoadRequested>(_onLoadRequested);
}
Future<void> _onLoadRequested(
TodoLoadRequested event,
Emitter<TodoState> emit,
) async {
emit(state.copyWith(
eventStatuses: state.eventStatuses.update(event, const LoadingEventStatus()),
));
try {
final todos = await loadTodos();
emit(state.copyWith(
todos: todos,
eventStatuses: state.eventStatuses.update(event, const SuccessEventStatus()),
));
} on Exception catch (e) {
emit(state.copyWith(
eventStatuses: state.eventStatuses.update(event, FailureEventStatus(e)),
));
}
}
}
Tip: An Emitter extension cleans this up significantly — see Tips.
Step 4: React in the UI #
Use standard flutter_bloc widgets. The EventStatusesMixin methods (statusOf, eventStatusOf, eventOf) slot directly into listenWhen / buildWhen / selector.
BlocListener — show a snackbar on failure
BlocListener<TodoBloc, TodoState>(
listenWhen: (previous, current) =>
previous.eventStatusOf<TodoLoadRequested>() !=
current.eventStatusOf<TodoLoadRequested>() &&
current.statusOf<TodoLoadRequested>() is FailureEventStatus,
listener: (context, state) {
final eventStatus = state.eventStatusOf<TodoLoadRequested>()!;
final error = (eventStatus.status as FailureEventStatus).error;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error loading todos: $error'),
action: SnackBarAction(
label: 'Retry',
onPressed: () => context.read<TodoBloc>().add(eventStatus.event),
),
),
);
},
child: child,
)
BlocSelector — switch on load status
BlocSelector<TodoBloc, TodoState, EventStatus?>(
selector: (state) => state.statusOf<TodoLoadRequested>(),
builder: (context, status) {
return switch (status) {
null => const SizedBox.shrink(),
LoadingEventStatus() => const CircularProgressIndicator(),
FailureEventStatus() => const Text('Error loading todos'),
SuccessEventStatus() => const TodoListView(),
};
},
)
BlocBuilder — show a spinner per-item
BlocBuilder<TodoBloc, TodoState>(
buildWhen: (previous, current) =>
current.eventOf<TodoDeleted>()?.todo.id == todo.id &&
previous.eventStatusOf<TodoDeleted>() !=
current.eventStatusOf<TodoDeleted>() &&
(previous.statusOf<TodoDeleted>() is LoadingEventStatus ||
current.statusOf<TodoDeleted>() is LoadingEventStatus),
builder: (context, state) {
if (state.statusOf<TodoDeleted>() is LoadingEventStatus) {
return const CircularProgressIndicator();
}
return IconButton(
icon: const Icon(Icons.delete),
onPressed: () => context.read<TodoBloc>().add(TodoDeleted(todo)),
);
},
)
API Reference #
EventStatuses<TEvent, TStatus> #
Immutable class (extends Equatable) that stores the status of each event type.
| Member | Description |
|---|---|
const EventStatuses() |
Creates an empty instance (use as initial value). |
update<TEventSubType>(event, status) |
Returns a new EventStatuses with the entry for TEventSubType updated. |
statusOf<TEventSubType>() |
Returns the current TStatus for TEventSubType, or null. |
eventOf<TEventSubType>() |
Returns the last TEventSubType instance that was updated, or null. |
eventStatusOf<TEventSubType>() |
Returns the full EventStatusUpdate record ({event, status}) for TEventSubType, or null. |
lastEventStatus |
Returns the most recently updated EventStatusUpdate, regardless of event type. |
EventStatusesMixin<TEvent, TStatus> #
Optional mixin for your BLoC state. Requires you to implement EventStatuses<TEvent, TStatus> get eventStatuses. Delegates all four query methods (statusOf, eventOf, eventStatusOf, lastEventStatus) to eventStatuses, so you can call them directly on the state.
EventStatusUpdate<TEvent, TStatus> #
A record typedef: ({TEvent event, TStatus status}). Returned by eventStatusOf and lastEventStatus.
Tips #
Emitter extension for cleaner Bloc code #
An extension on Emitter removes the copyWith boilerplate from every handler:
extension _TodoEmitterX on Emitter<TodoState> {
void _emit<T extends TodoEvent>(T event, EventStatus status, TodoState state) {
this(state.copyWith(
eventStatuses: state.eventStatuses.update(event, status),
));
}
void loading<T extends TodoEvent>(T event, TodoState state) =>
_emit(event, const LoadingEventStatus(), state);
void success<T extends TodoEvent>(T event, TodoState state) =>
_emit(event, const SuccessEventStatus(), state);
void failure<T extends TodoEvent>(T event, TodoState state, {required Exception error}) =>
_emit(event, FailureEventStatus(error), state);
}
Prefer code generation? The bloc_event_status_generator package can auto-generate this extension for you. Annotate your Bloc with
@blocEventStatusand runbuild_runner— see the generator README for setup instructions.
Usage in the handler:
Future<void> _onLoadRequested(
TodoLoadRequested event,
Emitter<TodoState> emit,
) async {
emit.loading(event, state);
try {
final todos = await loadTodos();
emit.success(event, state.copyWith(todos: todos));
} on Exception catch (e) {
emit.failure(event, state, error: e);
}
}
Access the triggering event for retry #
You can access the event instance that produced the last status update for any event. This is useful for retry actions — pass the original event back to the bloc:
listener: (context, state) {
final event = state.eventOf<TodoLoadRequested>()!; // Equivalent to `state.eventStatusOf<TodoLoadRequested>()!.event`
// Re-add the exact same event that failed
context.read<TodoBloc>().add(event);
},
Observe any status change with lastEventStatus #
lastEventStatus returns the most recent update regardless of event type. Use it to drive a global loading indicator or activity log:
BlocSelector<TodoBloc, TodoState, EventStatusUpdate<TodoEvent, EventStatus>?>(
selector: (state) => state.lastEventStatus,
builder: (context, lastStatus) {
if (lastStatus?.status is LoadingEventStatus) {
return const LinearProgressIndicator();
}
return const SizedBox.shrink();
},
)
Example #
See the example folder for a complete working app.
Acknowledgments #
A special thanks to LeanCode for their inspiring work on the bloc_presentation package, which served as a foundational reference and inspiration for the initial version of this project.
Contributing #
We welcome contributions! Please open an issue, submit a pull request or open a discussion on GitHub.
License #
This project is licensed under the MIT License.