skip to content

With package:bloc, how does a Bloc turn an event passed to add into one or more new states?

level: juniorimportance: must knowfreq 70%

answer

  1. events in, states out
  2. on<E> registered in the constructor
  3. handler gets event and Emitter
  4. zero or more emits per event
  5. one handler per event type

basics

~20 s

A Bloc receives event objects through add. Each event type has one handler registered with on<E> in the constructor; the handler gets the event and an Emitter and calls it zero or more times, often around an await, to produce new states.

solid answer

~40 s

`Bloc<Event, State>` extends `BlocBase`, so it has `state`, `stream` and `close()` and takes its initial state through `super`. In the constructor you register handlers such as `on<CitySearchQueryChanged>(_onQueryChanged)`. Callers, usually widgets, call `bloc.add(CitySearchQueryChanged('Lis'))`. The Bloc calls `onEvent`, routes the event to the handler whose type matches, and passes it an `Emitter<State>`; the handler may be `async` and can call `emit(...)` any number of times — typically in-progress, then success or failure. Each emit produces a `Transition` carrying the event, then passes the usual duplicate check. Rules: one handler per event type (a second `on<E>` for the same type throws a `StateError` in debug builds), `add` for a type with no handler also throws in debug, and states come only from handlers — not from public methods on the Bloc.

code

dart · 53 lines
dart
import 'package:bloc/bloc.dart';

sealed class CitySearchEvent {}

final class CitySearchQueryChanged extends CitySearchEvent {
  CitySearchQueryChanged(this.query);
  final String query;
}

final class CitySearchCleared extends CitySearchEvent {}

sealed class CitySearchState {}

final class CitySearchInitial extends CitySearchState {}

final class CitySearchInProgress extends CitySearchState {}

final class CitySearchSuccess extends CitySearchState {
  CitySearchSuccess(this.cities);
  final List<String> cities;
}

final class CitySearchFailure extends CitySearchState {
  CitySearchFailure(this.message);
  final String message;
}

abstract interface class CityRepository {
  Future<List<String>> search(String query);
}

class CitySearchBloc extends Bloc<CitySearchEvent, CitySearchState> {
  CitySearchBloc(this._cities) : super(CitySearchInitial()) {
    on<CitySearchQueryChanged>(_onQueryChanged);
    on<CitySearchCleared>((event, emit) => emit(CitySearchInitial()));
  }

  final CityRepository _cities;

  Future<void> _onQueryChanged(
    CitySearchQueryChanged event,
    Emitter<CitySearchState> emit,
  ) async {
    if (event.query.isEmpty) return emit(CitySearchInitial());
    emit(CitySearchInProgress());
    try {
      final cities = await _cities.search(event.query);
      emit(CitySearchSuccess(cities));
    } catch (_) {
      emit(CitySearchFailure('Could not load cities'));
    }
  }
}

go deeper

for a junior

Recall the flow: event classes, on<E> registration in the constructor, bloc.add from the widget, and emit called inside the handler.

for a middle

Explain what add does step by step, how handlers match by type, why one handler per type is enforced, and what a Transition records.

for a senior

Show design judgement: past-tense sealed events, private internal events, no public methods on a Bloc, and awaiting everything a handler starts.

for a principal

Set conventions for event granularity and naming across teams so Transition logs stay readable and handlers stay single-purpose.

## The moving parts In the **bloc** package (bloc 9.2), a `Bloc<Event, State>` converts a stream of **events** (inputs) into a stream of **states** (outputs). The pieces: - **Event classes** — usually a `sealed` base such as `CitySearchEvent` with `final` subclasses. The bloc naming conventions put events in the past tense (`CitySearchQueryChanged`), because from the Bloc's point of view they have already happened. - **State classes** — snapshots such as `CitySearchInitial`, `CitySearchInProgress`, `CitySearchSuccess`, `CitySearchFailure`. - **Handlers** — functions of type `EventHandler<E, State>`: `FutureOr<void> Function(E event, Emitter<State> emit)`. - **The Emitter** — the object a handler calls to publish a state. It also exposes `isDone`, `forEach` and `onEach`. ## Registering handlers ```dart class CitySearchBloc extends Bloc<CitySearchEvent, CitySearchState> { CitySearchBloc(this._cities) : super(CitySearchInitial()) { on<CitySearchQueryChanged>(_onQueryChanged); on<CitySearchCleared>((event, emit) => emit(CitySearchInitial())); } final CityRepository _cities; Future<void> _onQueryChanged( CitySearchQueryChanged event, Emitter<CitySearchState> emit, ) async { emit(CitySearchInProgress()); try { emit(CitySearchSuccess(await _cities.search(event.query))); } catch (_) { emit(CitySearchFailure('Could not load cities')); } } } ``` `on<E>` is called in the constructor body, so every handler exists before the first event can arrive. Matching uses `event is E`, so a handler registered for a base type receives every subtype. ## What add does 1. In debug builds, `add` checks that some handler accepts the event's type and throws a `StateError` naming the missing `on<...>` if none does. 2. It calls `onEvent`, which notifies the global observer. 3. It pushes the event into the Bloc's internal event stream. After `close()`, that stream is closed and `add` throws. 4. Each `on<E>` registration listens to that stream filtered to type `E`, passed through its **event transformer** — concurrent by default. 5. The handler runs with a fresh `Emitter`. Each `emit(next)` is dropped if the Bloc is closed or `next == state` after an earlier emit; otherwise `onTransition` receives a `Transition(currentState, event, nextState)` and the state is published. 6. When the handler's `Future` completes, its Emitter is marked done, and `onDone` is called for the event. ## Rules the library enforces | Situation | Result | |---|---| | `on<E>` twice for the same `E` | `StateError` in debug: called multiple times | | `add` with no matching handler | `StateError` in debug naming the missing handler | | `add` after `close()` | `StateError` | | `emit` after the handler completed | debug assertion (unawaited future) | | handler throws | `onError`, then `onDone` with the error, then rethrown | ## Conventions worth following - **Emit only inside handlers.** The bloc docs say a Bloc should never emit directly; every state change answers an event, which is what makes a `Transition` meaningful. - **No custom public methods on a Bloc.** Widgets call `add`. bloc_lint's recommended rule `avoid_public_bloc_methods` flags anything else. - **Private internal events** for stimuli that come from inside the Bloc, such as a repository stream; the leading underscore keeps widgets from adding them. - **One responsibility per handler.** A handler that grows branches for unrelated inputs usually wants a second event type. Since bloc 7.2 this `on<E>` API has been the only way in; the older `mapEventToState` generator was removed in bloc 8.0 and appears only in old tutorials.

  • Why must the handler await the repository call instead of using then?
    Bloc treats the handler's returned Future as the lifetime of that event's processing. With `then`, the handler returns at once, bloc marks its Emitter done, and the later `emit` trips a debug assertion about an unawaited future. `async` plus `await` keeps the Emitter alive until the result has been emitted.
  • When is it acceptable for a Bloc to add events to itself?
    The bloc FAQ allows it mainly when a repository stream, not the user, is the stimulus: the Bloc subscribes and adds a private event such as `_CitySearchHistoryChanged` for each value. Keeping the event private stops widgets from adding it, and it still gets its own handler and transformer.

saying these in an interview costs you the question

  • A handler returns the next state instead of calling emit
  • Registering on<E> twice for one type runs both handlers
  • Widgets should call bloc.emit to push a new state
  • A handler may emit only once per event
  • add() runs the handler synchronously before it returns