skip to content

Observers & Hydration

BlocObserver sees every bloc's creation, changes, transitions, errors and closing in one place, and hydrated_bloc restores state after a restart. Interviewers ask about global logging and persistence.

on this pageshow

explore

questions

5

In the bloc package, what is a BlocObserver for, and how do you register one so it sees every Bloc and Cubit?

level: juniorimportance: should knowfreq 45%

answer

  1. one place for every instance
  2. subclass and call super
  3. static Bloc.observer in main
  4. captured when each bloc is built
  5. MultiBlocObserver since 9.2

basics

~20 s

A BlocObserver receives lifecycle callbacks - creation, events, changes, transitions, errors, handler completion and closing - from every Bloc and Cubit in one place. Subclass it, call super in each override, and assign it to Bloc.observer in main before any bloc exists.

solid answer

~40 s

`BlocObserver` is the bloc package's global hook: its `onCreate`, `onEvent`, `onChange`, `onTransition`, `onError`, `onDone` and `onClose` callbacks fire for every `Bloc` and `Cubit` in the app, which makes it the natural place for debug logging or forwarding errors to a reporting service. You subclass it, override the callbacks you need (each is `@mustCallSuper`), and assign an instance to the static `Bloc.observer`, usually on the first lines of `main`. Timing matters: each bloc copies `Bloc.observer` into a field when it is constructed, so blocs created before the assignment keep reporting to the previous observer. To combine several observers, bloc 9.2 added `MultiBlocObserver(observers: [...])`; the zone-based `BlocOverrides` API was removed in bloc 9.0.

code

dart · 24 lines
dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class AppBlocObserver extends BlocObserver {
  const AppBlocObserver();

  @override
  void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
    super.onChange(bloc, change);
    debugPrint('${bloc.runtimeType} $change');
  }

  @override
  void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
    debugPrint('${bloc.runtimeType} $error');
    super.onError(bloc, error, stackTrace);
  }
}

void main() {
  Bloc.observer = const AppBlocObserver();
  runApp(const ReaderApp());
}

go deeper

for a junior

Remember that BlocObserver is a single class that sees every bloc and cubit, and that you assign it to Bloc.observer at the start of main.

for a middle

Explain that each instance captures the observer at construction, list which callbacks a Cubit never triggers, and show MultiBlocObserver for several concerns.

for a senior

Keep observers cheap, split logging from error forwarding, and make sure tests install their observer before any bloc is built.

for a principal

Decide what the global observer is allowed to do across teams - logging, error forwarding, lifecycle metrics - and what must stay out of it for performance and privacy.

## What a BlocObserver is In the **bloc** package (version 9 at the time of writing, re-exported by `flutter_bloc` and `hydrated_bloc`), every `Bloc` and every `Cubit` reports its lifecycle to one shared object: a **`BlocObserver`**. It is an abstract class with empty default implementations, so you override only what you need: | Callback | Fires when | Bloc | Cubit | |---|---|---|---| | `onCreate(bloc)` | an instance is constructed | yes | yes | | `onEvent(bloc, event)` | `add(event)` is called | yes | no | | `onChange(bloc, change)` | a new state is emitted | yes | yes | | `onTransition(bloc, transition)` | a handler emits in response to an event | yes | no | | `onError(bloc, error, stackTrace)` | an error is reported through `onError` | yes | yes | | `onDone(bloc, event, [error, stackTrace])` | an event handler finishes | yes | no | | `onClose(bloc)` | `close()` is called | yes | yes | Each callback is marked `@protected` and `@mustCallSuper`, so an override should call `super.onX(...)`. ## Registering it The registration point is a static field on `Bloc`: 1. Write a subclass - for example `AppBlocObserver extends BlocObserver`. 2. In `main`, before `runApp` and before anything creates a bloc, assign `Bloc.observer = const AppBlocObserver();`. 3. Every `Bloc` and `Cubit` constructed afterwards reports to it, including a `HydratedCubit` such as a reading-progress cubit. The detail interviewers probe is **when** the observer is read. In the bloc source, `BlocBase` stores `Bloc.observer` in a field as the instance is created, and all later callbacks go to that stored observer. So: - a bloc created before the assignment never reports to the new observer; - replacing `Bloc.observer` mid-session only affects blocs created afterwards; - tests that swap the observer temporarily must do so before building the bloc. ## More than one observer Before bloc 9.2 you had to write one observer that did everything. Since 9.2, **`MultiBlocObserver`** takes a list and forwards every callback to each observer in the order given: ```dart Bloc.observer = MultiBlocObserver( observers: [ const LoggingObserver(), const ErrorReportingObserver(), ], ); ``` This keeps concerns apart: a logging observer can be debug-only, while an error-forwarding one runs in every build. ## What it is good for - **Debug logging** of every change or transition without touching each bloc. - **Error forwarding** from `onError` to whatever crash-reporting setup the app uses. - **Lifecycle diagnostics**: `onCreate` shows when a lazily created cubit really appears, `onClose` shows whether it was disposed. Callbacks run synchronously on every change, so keep them cheap: format a log line, do not serialise large states or perform I/O inline. ## History worth knowing - bloc 8.0 replaced the static `Bloc.observer` with the zone-based `BlocOverrides.runZoned`; bloc 8.1 reintroduced `Bloc.observer` and deprecated `BlocOverrides`; bloc 9.0 removed `BlocOverrides` entirely. - bloc 9.1 added `onDone`. - bloc 9.2 added `MultiBlocObserver`. An answer that registers observers with `BlocOverrides` describes code that no longer compiles against bloc 9.

  • Why might a bloc created in a test never call your test observer?
    Each bloc stores `Bloc.observer` in a field when it is constructed. If the test assigns its observer after building the bloc, that instance keeps reporting to the observer that was active at construction. Assign the observer first, then build the bloc.
  • Should a BlocObserver's onChange serialise the whole state for logging?
    Usually not. Callbacks run synchronously on every emission, so heavy work there slows every state change in the app. Log a compact description in debug builds, and keep production observers to cheap work such as forwarding errors.

A BlocObserver is like the control room for a building's cameras: each camera is wired to whichever room was active on the day it was installed. Open a new control room later and the old cameras keep feeding the old one.

saying these in an interview costs you the question

  • Registers observers with BlocOverrides.runZoned in a bloc 9 codebase.
  • Sets Bloc.observer inside a widget after the blocs were created.
  • Believes assigning Bloc.observer twice adds a second observer.
  • Overrides observer callbacks without calling super.
  • Does network I/O synchronously inside onChange for every state.
open as a page

In the bloc package, what does addError do on a Cubit, and how does it differ from an exception thrown inside a Bloc event handler?

level: middleimportance: should knowfreq 30%

basics

~20 s

addError reports a caught error through the instance's onError to BlocObserver.onError without changing state or throwing. A Bloc handler that throws is reported the same way, then onDone receives the error and the exception is rethrown as an uncaught async error.

open as a page

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?

level: middleimportance: should knowfreq 35%

basics

~20 s

onEvent 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.

open as a page

With hydrated_bloc, how do you persist a reading-progress Cubit across app restarts using HydratedCubit, fromJson, toJson and HydratedStorage?

level: middleimportance: should knowfreq 25%

basics

~10 s

Await HydratedStorage.build and assign it to HydratedBloc.storage before runApp, extend HydratedCubit, and implement fromJson and toJson. The saved state is read synchronously in the constructor, and every later change is written back.

open as a page

After a release changes a HydratedCubit's state shape, saved JSON from the old version no longer parses; how does hydrated_bloc 11 react, and how do you migrate safely?

level: seniorimportance: nice to knowfreq 15%

basics

~20 s

If fromJson throws, hydrated_bloc reports it through onError, starts from the initial state and, by default, immediately overwrites the saved data. Migrate by versioning the JSON and reading old shapes in fromJson, with a stable storagePrefix.

open as a page