flutter
/

BLoC State Logging: Debugging & Monitoring with BlocObserver

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC State Logging: Debugging & Monitoring with BlocObserver

Introduction

Logging is essential for understanding the behavior of your BLoCs, debugging issues, and monitoring production apps. BLoC provides a built‑in BlocObserver that allows you to intercept events, transitions, and errors across all BLoCs and Cubits in your app. This guide covers how to implement comprehensive logging, integrate with Flutter DevTools, send logs to external services, and follow best practices for production‑ready logging.

Why Log BLoC State?

  • Debugging – Trace the flow of events and state changes to understand what happened before a bug.
  • Monitoring – Track performance, error rates, and user behavior in production.
  • Testing – Validate that events and transitions occur as expected during manual testing.
  • Documentation – Logs serve as a runtime record of app behavior.
  • Performance Analysis – Measure how long transitions take (with timestamps).

Setting Up a Custom BlocObserver

The simplest way to start logging is to create a custom BlocObserver that overrides onEvent, onTransition, and onError. Set it globally before running the app.

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

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

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

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

Logging with Timestamps

Add timestamps to logs to measure performance and order of events. This is particularly useful for debugging race conditions or slow transitions.

DARTRead-only
1
class TimestampBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    final timestamp = DateTime.now().toIso8601String();
    print('[$timestamp] EVENT ${bloc.runtimeType}: $event');
    super.onEvent(bloc, event);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    final timestamp = DateTime.now().toIso8601String();
    print('[$timestamp] TRANSITION ${bloc.runtimeType}: $transition');
    super.onTransition(bloc, transition);
  }
}

Logging to Flutter DevTools

Use dart:developer's log function to send logs to the DevTools logging view. This allows filtering, searching, and seeing logs alongside other app events.

DARTRead-only
1
import 'dart:developer' as developer;

class DevToolsBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    developer.log('$event', name: '${bloc.runtimeType}.event');
    super.onEvent(bloc, event);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    developer.log(
      '${transition.currentState} -> ${transition.nextState}',
      name: '${bloc.runtimeType}.transition',
    );
    super.onTransition(bloc, transition);
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    developer.log(
      error.toString(),
      name: '${bloc.runtimeType}.error',
      error: error,
      stackTrace: stackTrace,
    );
    super.onError(bloc, error, stackTrace);
  }
}

Conditional Logging (Debug vs Production)

Avoid logging sensitive data or performance overhead in production. Use kDebugMode to enable logging only in development.

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

Structured Logging with Logger Package

For production apps, consider using a structured logging package like logger to output JSON or formatted logs that can be sent to remote services.

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

class LoggerBlocObserver extends BlocObserver {
  final Logger _logger = Logger(printer: PrettyPrinter());

  @override
  void onEvent(Bloc bloc, Object? event) {
    _logger.d('${bloc.runtimeType} event: $event');
    super.onEvent(bloc, event);
  }

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

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

Remote Logging & Analytics

In production, you may want to send logs to a remote service (e.g., Firebase Crashlytics, Sentry). Use onError to capture unhandled errors and onTransition to log critical state changes.

DARTRead-only
1
class RemoteBlocObserver extends BlocObserver {
  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    // Send to crash reporting service
    FirebaseCrashlytics.instance.recordError(error, stackTrace);
    super.onError(bloc, error, stackTrace);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    // Log important transitions (e.g., login, payment)
    if (transition.nextState is LoginSuccess || transition.nextState is PaymentComplete) {
      Analytics.logEvent(
        name: 'state_change',
        parameters: {
          'bloc': bloc.runtimeType.toString(),
          'state': transition.nextState.runtimeType.toString(),
        },
      );
    }
    super.onTransition(bloc, transition);
  }
}

Logging Specific BLoCs Only

If you want to log only certain BLoCs (e.g., during development of a feature), you can filter inside the observer.

DARTRead-only
1
class FilteredBlocObserver extends BlocObserver {
  final List<Type> allowedBlocs = [AuthBloc, ProfileBloc];

  @override
  void onEvent(Bloc bloc, Object? event) {
    if (allowedBlocs.contains(bloc.runtimeType)) {
      print('${bloc.runtimeType} event: $event');
    }
    super.onEvent(bloc, event);
  }
}

Measuring Transition Duration

Use BlocObserver to measure how long a transition takes, helping identify performance bottlenecks.

DARTRead-only
1
class PerformanceBlocObserver extends BlocObserver {
  final Stopwatch _stopwatch = Stopwatch();

  @override
  void onEvent(Bloc bloc, Object? event) {
    _stopwatch.reset();
    _stopwatch.start();
    super.onEvent(bloc, event);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    _stopwatch.stop();
    print('${bloc.runtimeType} transition for ${transition.event} took ${_stopwatch.elapsedMilliseconds}ms');
    super.onTransition(bloc, transition);
  }
}

Best Practices

  • Enable logging only in debug builds – Avoid performance hit and sensitive data leaks in production.
  • Use structured logging – JSON logs are easier to parse by external tools.
  • Log at appropriate levels – Use info for normal transitions, warning for recoverable errors, error for exceptions.
  • Avoid logging sensitive data – Do not log passwords, tokens, or personally identifiable information (PII).
  • Include timestamps – Essential for debugging race conditions and order of events.
  • Test observers – Ensure they don’t throw exceptions and handle malformed data gracefully.
  • Rotate logs or limit size – If writing to file, implement log rotation to avoid disk bloat.

Common Mistakes

  • ❌ Logging everything in production – Can slow down the app and expose internal state. ✅ Use conditional logging or remote logging with sampling.
  • ❌ Forgetting to call super methods – May break internal BLoC functionality. ✅ Always call super.onEvent/onTransition/onError.
  • ❌ Logging huge state objects – Clutters logs and impacts performance. ✅ Log only essential fields or summary.
  • ❌ Not handling errors in observers – Observers themselves can throw, crashing the app. ✅ Wrap logging in try‑catch if necessary.
  • ❌ Using print in production – print is not suitable for production; use proper logging packages.

Conclusion

Effective logging of BLoC events and state transitions is a powerful tool for debugging and monitoring. By leveraging BlocObserver, you can gain full visibility into your app’s state flow. Combine with DevTools, structured logs, and remote services to create a robust observability layer that helps you ship with confidence.

Try it yourself

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

void main() {
  // Set up observer for demo
  Bloc.observer = SimpleBlocObserver();
  runApp(MyApp());
}

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

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

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

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

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

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cubit = context.watch<CounterCubit>();
    return Scaffold(
      appBar: AppBar(title: Text('BLoC Logging Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Counter:', style: TextStyle(fontSize: 20)),
            Text('${cubit.state}', style: TextStyle(fontSize: 40)),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: cubit.increment,
                  child: Icon(Icons.add),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: cubit.decrement,
                  child: Icon(Icons.remove),
                ),
              ],
            ),
            SizedBox(height: 20),
            Text('Check the console for logged events and transitions!'),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which class should you extend to globally observe BLoC events and transitions?

A
BlocObserver
B
BlocListener
C
BlocProvider
D
BlocBuilder
Q2
of 3

What method in BlocObserver is called when a state changes?

A
onEvent
B
onTransition
C
onStateChange
D
onEmit
Q3
of 3

How do you enable logging only in development builds?

A
Use conditional import
B
Check kDebugMode before setting Bloc.observer
C
Wrap logs with if (kDebugMode)
D
Both B and C

Frequently Asked Questions

Can I have multiple BlocObserver instances?

No, you can only set one Bloc.observer. If you need multiple observers, you can create a composite observer that delegates to multiple internal observers.

How do I log only specific events or states?

Inside onEvent or onTransition, you can check the type of event or state and conditionally log.

Does BlocObserver work with Cubit?

Yes, BlocObserver also observes Cubit transitions (events are not applicable for Cubits).

How can I send logs to Firebase Crashlytics?

Use FirebaseCrashlytics.instance.log() inside onEvent or onTransition to send custom logs, or recordError for errors.

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

onEvent is called when an event is added to the BLoC. onTransition is called when the state changes as a result of processing an event (i.e., after mapEventToState yields a new state).

Previous

bloc observer

Related Content

Need help?

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