flutter
/

BlocObserver: The Complete Guide to Observing BLoC & Cubit Lifecycles

Last Sync: Today

On this page

14
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BlocObserver: The Complete Guide to Observing BLoC & Cubit Lifecycles

What is BlocObserver?

BlocObserver is an abstract class provided by the flutter_bloc package that allows you to observe every BLoC and Cubit instance in your application. By extending it, you can intercept key lifecycle events: when an event is added to a BLoC, when a state change occurs (transition), when any state change happens (for both BLoC and Cubit), and when an error is thrown. This gives you powerful capabilities for logging, debugging, analytics, and crash reporting.

Why Use a BlocObserver?

  • Centralized Logging – Log all events and state changes in one place, not scattered across BLoCs.
  • Debugging – Track the exact sequence of events and states leading to a bug.
  • Analytics – Send user actions (events) and state data to analytics platforms.
  • Crash Reporting – Attach BLoC context to crash reports for easier debugging.
  • Performance Monitoring – Measure how often events are processed and how long transitions take.
  • Auditing – Keep an immutable record of state changes for compliance or debugging.

Key Methods of BlocObserver

BlocObserver provides four main methods you can override:

  • onEvent – Called when an event is added to a BLoC (not Cubits).
  • onTransition – Called when a BLoC transitions from one state to another (includes event, currentState, nextState).
  • onChange – Called when any BlocBase (BLoC or Cubit) changes state. This is the most generic method.
  • onError – Called when an error is thrown inside a BLoC or Cubit.

Creating a Custom BlocObserver

Extend BlocObserver and override the methods you need. Don't forget to call super to maintain the observer chain (if you have multiple observers).

DARTRead-only
1
class SimpleBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    print('${bloc.runtimeType} Event: $event');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('${bloc.runtimeType} Transition: $transition');
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} Change: $change');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    super.onError(bloc, error, stackTrace);
    print('${bloc.runtimeType} Error: $error');
  }
}

Registering the Observer

Set the observer before running your app, typically in main().

DARTRead-only
1
void main() {
  Bloc.observer = SimpleBlocObserver();
  runApp(MyApp());
}

Understanding onEvent vs onTransition vs onChange

These three methods serve different purposes and are called at different moments in a BLoC's lifecycle.

MethodWhen CalledParametersAvailable for
`onEvent`Immediately when an event is added to a BLoC`bloc`, `event`BLoC only
`onTransition`After a state change occurs in a BLoC (includes event)`bloc`, `transition`BLoC only
`onChange`After any state change in a BLoC or Cubit`bloc`, `change`BLoC & Cubit

For most logging and debugging, onChange is sufficient because it covers both BLoC and Cubit. If you need the event that triggered the change, use onTransition for BLoCs.

Logging with BlocObserver

A common use case is to log everything to the console. You can use a logging package like logger for structured logs.

DARTRead-only
1
import 'package:logger/logger.dart';

class LoggingBlocObserver extends BlocObserver {
  final Logger logger = Logger();

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    logger.d('${bloc.runtimeType} - $change');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    super.onError(bloc, error, stackTrace);
    logger.e('${bloc.runtimeType} Error: $error', error: error, stackTrace: stackTrace);
  }
}

Analytics Integration

Use BlocObserver to automatically send analytics events for user actions. For example, you can send an event name based on the BLoC and event type.

DARTRead-only
1
class AnalyticsBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    // Send to Firebase Analytics or similar
    FirebaseAnalytics.instance.logEvent(
      name: 'bloc_event',
      parameters: {
        'bloc': bloc.runtimeType.toString(),
        'event': event.runtimeType.toString(),
      },
    );
  }
}

Crash Reporting with BlocObserver

Capture errors from BLoCs and Cubits and send them to crash reporting services.

DARTRead-only
1
class CrashlyticsBlocObserver extends BlocObserver {
  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    super.onError(bloc, error, stackTrace);
    FirebaseCrashlytics.instance.recordError(
      error,
      stackTrace,
      reason: 'Error in ${bloc.runtimeType}',
    );
  }
}

Performance Monitoring

Measure the duration of transitions to identify slow BLoCs.

DARTRead-only
1
class PerformanceBlocObserver extends BlocObserver {
  final Map<BlocBase, DateTime> _startTimes = {};

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    final duration = DateTime.now().difference(_startTimes[bloc]!);
    print('${bloc.runtimeType} transition took ${duration.inMilliseconds}ms');
    _startTimes.remove(bloc);
  }
}

Composing Multiple Observers

You can only set one global observer. To have multiple responsibilities, create a composite observer that delegates to several handlers.

DARTRead-only
1
class CompositeBlocObserver extends BlocObserver {
  final List<BlocObserver> observers;

  CompositeBlocObserver(this.observers);

  @override
  void onChange(BlocBase bloc, Change change) {
    for (var observer in observers) {
      observer.onChange(bloc, change);
    }
  }

  // similarly override other methods
}

void main() {
  Bloc.observer = CompositeBlocObserver([
    LoggingBlocObserver(),
    AnalyticsBlocObserver(),
  ]);
  runApp(MyApp());
}

Best Practices

  • Keep observers lightweight – Avoid heavy operations; they run on the UI thread.
  • Use async logging carefully – If logging to a file or network, do it asynchronously to avoid blocking.
  • Don't mutate state inside observers – Observers are for observation only.
  • Always call super methods – Ensures any internal observer chain (like DevTools) still works.
  • Conditionally enable observers – Only enable verbose logging in debug mode.
  • Use Bloc.observer only once – Set it in main() before the app runs.
  • Test observers separately – Mock the bloc and verify that the observer methods are called.

Common Mistakes

  • ❌ Forgetting to call super – Breaks the observer chain (including the built-in DevTools observer).
  • ❌ Accessing BuildContext in observer – Observers don't have a context; use a global key or stream if you need UI feedback.
  • ❌ Assuming onTransition is called for Cubits – It's not; Cubits only trigger onChange.
  • ❌ Logging sensitive data – Events may contain passwords or tokens; redact them.
  • ❌ Setting observer multiple times – Only the last assignment takes effect; set once.
  • ❌ Heavy synchronous work inside observer – Can cause jank.

Conclusion

BlocObserver is an invaluable tool for understanding what's happening inside your BLoCs and Cubits. Whether you're debugging, adding analytics, or monitoring performance, a well‑crafted observer provides deep insights without polluting your business logic. By following best practices, you can harness its power to build more robust Flutter applications.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

// ---------- Custom BlocObserver ----------
class SimpleBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    print('🎯 EVENT: ${bloc.runtimeType} - $event');
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('🔄 CHANGE: ${bloc.runtimeType} - $change');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('⚡ TRANSITION: ${bloc.runtimeType} - $transition');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    super.onError(bloc, error, stackTrace);
    print('❌ ERROR: ${bloc.runtimeType} - $error');
  }
}

// ---------- Counter BLoC ----------
class CounterState extends Equatable {
  final int value;
  const CounterState(this.value);
  @override List<Object?> get props => [value];
}

abstract class CounterEvent extends Equatable {
  const CounterEvent();
  @override List<Object?> get props => [];
}

class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
class ThrowError extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<Increment>((event, emit) => emit(CounterState(state.value + 1)));
    on<Decrement>((event, emit) => emit(CounterState(state.value - 1)));
    on<ThrowError>((event, emit) {
      throw Exception('Simulated error from CounterBloc');
    });
  }
}

// ---------- App ----------
void main() {
  Bloc.observer = SimpleBlocObserver();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('BlocObserver Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BlocBuilder<CounterBloc, CounterState>(
              builder: (context, state) {
                return Text('Count: ${state.value}', style: TextStyle(fontSize: 32));
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => context.read<CounterBloc>().add(Increment()),
                  child: Text('+'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterBloc>().add(Decrement()),
                  child: Text('-'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterBloc>().add(ThrowError()),
                  child: Text('Throw Error'),
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                ),
              ],
            ),
            SizedBox(height: 20),
            Text('Open the console to see the observer output!'),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 4

Which method of BlocObserver is called for every state change in both BLoCs and Cubits?

A
onEvent
B
onTransition
C
onChange
D
onError
Q2
of 4

How do you set a global BlocObserver?

A
BlocObserver.set(MyObserver())
B
Bloc.observer = MyObserver()
C
MaterialApp(observer: MyObserver())
D
It's automatically set
Q3
of 4

Which method is called only for BLoCs and includes the event that triggered the state change?

A
onEvent
B
onTransition
C
onChange
D
onError
Q4
of 4

Why should you always call `super` when overriding observer methods?

A
To ensure the observer chain (including DevTools) continues to work
B
To avoid compilation errors
C
It's optional
D
To prevent memory leaks

Frequently Asked Questions

Can I have multiple BlocObservers?

Only one global observer can be registered. To use multiple, create a composite observer that delegates to several others.

What's the difference between `onChange` and `onTransition`?

onChange is called for every state change in any BlocBase (both BLoC and Cubit). onTransition is called only for BLoCs and includes the event that triggered the transition.

Does BlocObserver work with HydratedBloc?

Yes, observers work with any subclass of BlocBase, including HydratedBloc and HydratedCubit.

Can I use BlocObserver to intercept events before they are processed?

Yes, onEvent is called immediately when an event is added, before it's passed to the event handler. You cannot prevent the event from being processed, but you can log it.

How do I test a custom BlocObserver?

Set it as the global observer in your test, create a bloc, dispatch an event, and verify that the observer's methods were called (using mocks or recording).

Previous

bloc caching strategy

Next

bloc state logging

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.