BlocEventStatus

BlocEventStatus CI codecov pub package pub points pub monthly downloads pub Likes License: MIT

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 @blocEventStatus and run build_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.

Libraries

bloc_event_status
A Flutter package that provides a way to manage the state of events in a Flutter application using the BLoC pattern.