bloc_event_status 2.0.0 copy "bloc_event_status: ^2.0.0" to clipboard
bloc_event_status: ^2.0.0 copied to clipboard

Track the status of events in a bloc without updating the state.

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.

1
likes
150
points
105
downloads

Documentation

Documentation
API reference

Publisher

verified publisherjacopoguzzo.dev

Weekly Downloads

Track the status of events in a bloc without updating the state.

Repository (GitHub)
View/report issues

Topics

#bloc #state-management

License

MIT (license)

Dependencies

bloc, equatable, meta

More

Packages that depend on bloc_event_status