In the bloc package, in what order do BlocObserver's onEvent, onTransition, onChange and onDone fire for one handled event, and what differs for a Cubit?
answer
- add fires first
- transition wraps the change
- both before state updates
- Cubit: onChange only
- equal state fires nothing
basics
~20 sonEvent fires when add is called; each emit then fires onTransition followed by onChange, both before the state updates; onDone fires when the handler finishes. A Cubit has no events, so its emit fires onChange only.
solid answer
~40 sIn bloc 9, `add(event)` calls the bloc's `onEvent`, which forwards to `BlocObserver.onEvent`, before the event is queued. When the `on<E>` handler calls `emit`, the bloc first calls `onTransition` with the event, current state and next state, then `onChange` with just the two states; both run **before** `state` is updated, so reading `bloc.state` inside them still gives the old value. When the handler's future completes, `onDone` fires, carrying the error and stack trace if it threw. A `Cubit` has no events, so `emit` produces only `onChange`. An emit of a state equal to the current one fires nothing. Per-instance overrides of these hooks are `@mustCallSuper`: skipping `super` silences the global observer for that bloc.
go deeper
Recall that onChange fires for every emit on both Bloc and Cubit, while onEvent and onTransition exist only for Bloc.
Walk through the full order - onEvent, onTransition, onChange, state update, onDone - and explain why bloc.state inside onChange is still the old value.
Diagnose missing logs or persistence caused by an override that skips super, and pick onChange over onTransition when the observer must cover cubits.
Define which hook a team's cross-cutting concerns use, so logging, analytics and persistence stay consistent across blocs and cubits.
## Two layers of hooks The bloc package offers each lifecycle hook twice: - **per instance** - `onEvent`, `onTransition`, `onChange`, `onError`, `onDone` as methods you can override on your own `Bloc` or `Cubit`; - **globally** - the same names on the app-wide `BlocObserver`, assigned to `Bloc.observer`. The per-instance method is the one the framework calls; its base implementation forwards to the observer. That is why the instance methods are `@mustCallSuper`: an override that forgets `super.onChange(change)` quietly cuts that bloc off from the global observer - and from anything else built on `onChange`, such as hydrated_bloc's persistence. ## The order for one handled event From the bloc 9 source, adding one event whose handler emits one new state runs: 1. **`onEvent(event)`** - synchronously inside `add`, before the event reaches the handler. 2. The handler runs (asynchronously, after the event is delivered). It calls `emit(next)`. 3. **`onTransition(Transition(currentState, event, nextState))`** - the bloc-specific record tying the event to the change. 4. **`onChange(Change(currentState, nextState))`** - the generic record every `BlocBase` produces. 5. The state is updated and pushed to `stream`, so `BlocBuilder`s rebuild. 6. **`onDone(event)`** - when the handler's future completes; if the handler threw, `onError` fires first and `onDone` receives the error and stack trace. A handler that emits twice produces two transition-plus-change pairs and one `onDone`. ## What differs for a Cubit A `Cubit` changes state through its own methods calling `emit`. There is no event, so: | Callback | Bloc | Cubit | |---|---|---| | `onEvent` | on every `add` | never | | `onTransition` | on every emit from a handler | never | | `onChange` | on every emit | on every emit | | `onDone` | when a handler finishes | never | | `onError` | reported errors | reported errors | This is why a logging observer that only overrides `onTransition` shows nothing for cubits: `onChange` is the callback that covers both. ## Before the state updates The documentation of both `onChange` and `onTransition` says they are called before the state is updated. In the source, `emit` calls `onChange` and only then assigns the new state. Practical consequences: - inside the callback, use `change.currentState` and `change.nextState`, not `bloc.state`, to see what is happening; - an exception thrown from an override interrupts that emit - the state is not updated. ## When nothing fires - **Equal state.** If the emitted state `==` the current one and the instance has emitted before, the emit is dropped before `onTransition` or `onChange`. (The very first emission is allowed even when equal.) - **Closed bloc.** Emitting from a handler after `close()` is ignored; calling `emit` directly on a closed instance throws a `StateError`, which is reported through `onError`. ## Why the order matters in interviews The order explains real bugs: - analytics in `onChange` that reads `bloc.state` logs the previous value; - a cubit with a custom `onChange` that skips `super` stops being logged and, if it is hydrated, stops being persisted; - a transition log that never shows a cubit is not broken - cubits have no transitions.
- Inside BlocObserver.onChange, what does bloc.state return?The previous state. `onChange` runs before the new state is assigned, so read `change.nextState` for the value being emitted and `change.currentState` for the one it replaces.
- Why can skipping super.onChange in a HydratedCubit stop persistence?hydrated_bloc persists inside its own `onChange` override, which runs through the `super` chain. An override on your cubit that omits `super.onChange(change)` never reaches it, so nothing is written - and the global observer sees nothing either.
saying these in an interview costs you the question
- Expects onTransition logs from a Cubit.
- Believes onChange runs after the state has already been updated.
- Thinks emitting an equal state still triggers onChange.
- Overrides onChange on a bloc without calling super.onChange.
- Places onEvent after the handler has emitted its states.