flutter
/

BLoC Logging: Comprehensive Guide to Debugging & Monitoring

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC Logging: Comprehensive Guide to Debugging & Monitoring

What is BLoC Logging?

BLoC logging refers to the practice of recording events, state changes, and errors that occur within your BLoCs and Cubits. Using a custom BlocObserver, you can intercept every event dispatched, every state emitted, and any error thrown. This gives you deep visibility into your app's behavior, making debugging easier, helping you track user flows, and enabling integration with analytics and crash reporting tools.

Why Use Logging in BLoC?

  • Debugging – Quickly identify what events led to a certain state.
  • Audit Trails – Understand user actions and system behavior.
  • Performance Monitoring – Track how often events are triggered.
  • Crash Reporting – Attach BLoC context to crash reports.
  • Analytics – Send meaningful user events to analytics platforms.
  • Testing – Verify that events and states flow as expected during development.

Setting Up a Basic BlocObserver

Create a class that extends BlocObserver and override the methods you want to log. Then set it as the global observer before running the app.

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

class SimpleBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} - $change');
  }

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

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

Advanced Logging with Logging Levels

For production, you may want to control verbosity. You can use a logging package like logger or create your own with levels (debug, info, warning, error).

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

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

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

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    logger.i('${bloc.runtimeType} - $transition');
  }

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

Logging Events with Custom Data

Sometimes you want to add context (e.g., user ID, session ID) to every log. You can extend the observer to include such data.

DARTRead-only
1
class ContextualBlocObserver extends BlocObserver {
  final String userId;

  ContextualBlocObserver(this.userId);

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

Integrating with Crash Reporting (Firebase Crashlytics)

You can send BLoC errors to crash reporting tools for analysis.

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

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}',
    );
  }
}

Logging Transitions vs Changes

onChange is called for every state change in any BlocBase (both BLoC and Cubit). onTransition is called only for BLoCs and provides the event that triggered the transition. Use onTransition when you need the event context.

DARTRead-only
1
@override
void onTransition(Bloc bloc, Transition transition) {
  super.onTransition(bloc, transition);
  print('${bloc.runtimeType} Event: ${transition.event} | CurrentState: ${transition.currentState} | NextState: ${transition.nextState}');
}

Formatting Logs for Readability

To make logs easier to parse, use consistent formatting and colors. The logger package can output colored logs in the console.

DARTRead-only
1
final logger = Logger(
  printer: PrettyPrinter(
    methodCount: 0,
    errorMethodCount: 5,
    lineLength: 80,
    colors: true,
    printEmojis: true,
  ),
);

Conditional Logging for Different Environments

You may want to enable verbose logging only in development. Use kReleaseMode to conditionally register observers.

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

void main() {
  if (kDebugMode) {
    Bloc.observer = VerboseBlocObserver();
  } else {
    // In release, maybe only errors
    Bloc.observer = ErrorOnlyBlocObserver();
  }
  runApp(MyApp());
}

Logging to Files or Remote Services

For production monitoring, you can send logs to a remote service (e.g., Sentry, Datadog). Use an observer that asynchronously sends data.

DARTRead-only
1
class RemoteLoggingObserver extends BlocObserver {
  final RemoteLoggingService loggingService;

  RemoteLoggingObserver(this.loggingService);

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

Best Practices

  • Use a single observer – One global observer is enough; compose multiple handlers inside it if needed.
  • Avoid heavy work in observers – Logging should be fast and non‑blocking.
  • Don’t log sensitive data – Avoid logging passwords, tokens, or PII.
  • Use log levels – Differentiate between debug, info, warning, error.
  • Attach context – Include user ID, session, or feature flags in logs when possible.
  • Format consistently – Makes logs easier to search and parse.
  • Test observer in isolation – Ensure it doesn’t break the app if the logging service fails.

Common Mistakes

  • ❌ Logging inside event handlers – Creates duplication and mixes concerns; use observer instead.
  • ❌ Forgetting to call super methods – Breaks internal observer chain.
  • ❌ Logging excessively in production – Can impact performance and fill storage.
  • ❌ Not sanitizing logs – May expose sensitive information.
  • ❌ Using print() instead of a proper logger – print is hard to control and disable.
  • ❌ Blocking the main thread with I/O – If writing to disk or network, do it asynchronously.

Conclusion

BLoC logging with a custom BlocObserver gives you unparalleled insight into your app's state management. Whether you're debugging locally or monitoring in production, logging helps you understand user flows, catch errors early, and improve overall app quality. By following best practices and integrating with modern logging and crash reporting tools, you can build robust, observable 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 onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    // In a real app, use a proper logger
    print('========================================');
    print('${bloc.runtimeType} - CHANGE');
    print('  currentState: ${change.currentState}');
    print('  nextState: ${change.nextState}');
  }

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

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    super.onError(bloc, error, stackTrace);
    print('${bloc.runtimeType} - ERROR: $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() {
  // Set the global observer
  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('BLoC Logging 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('Check the console for logs!', style: TextStyle(fontSize: 12)),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 4

Which class should you extend to create a custom BLoC logger?

A
BlocLogger
B
BlocObserver
C
BlocListener
D
BlocDelegate
Q2
of 4

What method is called for every state change in both BLoCs and Cubits?

A
onTransition
B
onChange
C
onEvent
D
onStateChanged
Q3
of 4

How do you set the global observer?

A
Bloc.observer = MyObserver()
B
BlocObserver.set(MyObserver())
C
MyObserver.register()
D
It's automatic
Q4
of 4

Why might you avoid logging in production?

A
Logs are always harmful
B
Performance impact and sensitive data exposure
C
Flutter doesn't support logging in release
D
Observers don't work in release mode

Frequently Asked Questions

Can I have multiple BlocObservers?

No, only one observer can be set globally. However, you can create a composite observer that delegates to multiple handlers (e.g., one for printing, one for analytics).

Will logging affect performance?

Minimally, if done correctly. Avoid heavy operations like synchronous file writes. Use asynchronous logging if necessary. In production, you can reduce verbosity.

How do I log Cubit changes?

onChange is called for both Cubit and BLoC state changes. Use that method to log state changes for Cubits.

Can I get the event that caused a state change in a Cubit?

No, Cubits don't have events. Use a BLoC if you need to log the triggering event.

How do I log only specific BLoCs?

In the observer, you can check bloc.runtimeType and filter. Or you can implement a conditional inside the method.

Previous

bloc code generation

Next

bloc devtools

Related Content

Need help?

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