skip to content

Event Handlers & Concurrency

A Bloc maps events to states: on<E> registers a handler that gets an Emitter, and an EventTransformer decides how overlapping events run. Interviewers probe droppable versus restartable.

on this pageshow

explore

questions

5

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
open as a page

With bloc_concurrency, how do the concurrent, sequential, droppable and restartable transformers differ, and when would you pick each?

level: middleimportance: must knowfreq 60%

basics

~20 s

They decide how overlapping events of one handler run: concurrent runs them all at once (the default), sequential queues them in order, droppable ignores new events while one runs, and restartable cancels the running one in favour of the newest.

open as a page

With package:bloc, why does a handler trigger the assertion 'emit was called after an event handler completed normally', and how do you fix it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

The handler's Future completed while work it started was still pending, typically a then callback, an unawaited helper or a Timer, and that work later called emit. Make the handler async and await everything that emits, or return its Future.

open as a page

With package:bloc, when should a Bloc handler follow a repository stream with emit.forEach or emit.onEach, and what must it do with the returned Future?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use emit.forEach when each stream value should become a state, and emit.onEach when each value needs custom handling; both subscribe for the handler's lifetime and must be awaited or returned, or bloc reports pending subscriptions.

open as a page