BlocObserver: Debug and Observe your Bloc Easily

BlocObserver: Debug and Observe your Bloc Easily

Learn how to observe a bloc by simply overriding `onChange`, `onTransition`, `onEvent`, `onError` methods.

ยท

5 min read

Introduction


BlocObserver

  • BlocObserver is a abstract class for monitoring BloC instances behavior. This means we can track anytime an event is triggered or a state is emitted. And in the instance of Debugging the BloC, I believe this is incredible.

    • onChange()

      • You must override the onChange method inside your bloc in order to see the change when a new state is emitted.

        class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
          // Your events here
          on<ExampleEvent>(....)
        
         @override
           void onChange(Change<ExampleState> change) {
              super.onChange(change);
                  debugPrint(change.toString());
                  debugPrint(change.currentState.toString());
                  debugPrint(change.nextState.toString());
             } 
        }
        
      • As you can see, the onChange function has one argument which is of type Change
      • A Change represents the change from one State to another.
      • A Change consists of the currentState and nextState.
      • Now whenever a new state is emitted a Change occurs, which is why onChange is a great spot to add logging/analytics for a specific cubit.

        One thing to remember here is you need to always call super.onChange first before performing any operation.

    • onTransition

      • If you want to observe a bloc whenever a new event is added and a new state is added, you can override onTransition method.
      • A transition occurs when a new event is added and a new state is emitted from a corresponding EventHandler executed.
      • onTransition is called before a Bloc's state has been updated.
      • Which is why it is a great spot to add logging/analytics at the individual Bloc level.
      class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
          // Your events here
          on<ExampleEvent>(....)
      
           @override
              void onTransition(Transition<JokeEvent, JokeState> transition) {
                 super.onTransition(transition);
                debugPrint(transition.toString());
          }
      }
      
      • onTransition is invoked before onChange

        super.onTransition should always be called first.

    • onError

      • To track a bloc's error whenever it happens, just override the onError function within your bloc.
      • onError is called whenever an error occurs and notifies BlocObserver.onError.

        class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
          // Your events here
          on<ExampleEvent>(....)
        
         @override
           void onError(Object error, StackTrace stackTrace) {
              super.onError(error, stackTrace);
              debugPrint(error.toString());
          }
        }
        

        super.onError should always be called first.

    • onEvent

      • As the name suggests whenever an event is added to the Bloc this method is triggered.
      • It is also a great spot to add logging/analytics at the individual Bloc level.
      class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
          // Your events here
          on<ExampleEvent>(....)
      
          @override
            void onEvent(JokeEvent event) {
             super.onEvent(event);
             debugPrint(event.toString());
         }
      }
      

Live Example

  • To show you the working of the BlocObserver I've taken an example that I've taken inside this bloc article.
class JokeBloc extends Bloc<JokeEvent, JokeState> {
  final JokeRepository _jokeRepository;

  JokeBloc(this._jokeRepository) : super(JokeLoadingState()) {
    on<LoadJokeEvent>(
      (event, emit) async {
        emit(JokeLoadingState());
        try {
          final joke = await _jokeRepository.getJoke();
          emit(JokeLoadedState(joke));
        } catch (e) {
          addError(Exception(e.toString()), StackTrace.current);
          emit(JokeErrorState(e.toString()));
        }
      },
    );
    on<ExtraLoadJokeEvent>(
      (event, emit) async {
        emit(JokeLoadingState());
        try {
          await Future.delayed(const Duration(seconds: 3));
          final joke = await _jokeRepository.getJoke();
          emit(JokeLoadedState(joke));
        } catch (e) {
          addError(Exception(e.toString()), StackTrace.current);
          emit(JokeErrorState(e.toString()));
        }
      },
    );
  }

  @override
  void onTransition(Transition<JokeEvent, JokeState> transition) {
    super.onTransition(transition);
    debugPrint(transition.toString());
  }

  @override
  void onChange(Change<JokeState> change) {
    super.onChange(change);
    debugPrint(change.toString());
    debugPrint(change.currentState.toString());
    debugPrint(change.nextState.toString());
  }

  @override
  void onError(Object error, StackTrace stackTrace) {
    super.onError(error, stackTrace);
    debugPrint(error.toString());
  }

  @override
  void onEvent(JokeEvent event) {
    super.onEvent(event);
    debugPrint(event.toString());
  }
}
  • Let's now run the app and observe the bloc and the flow of all the overridden methods.

bloc observer.gif

Flow of Overriden Observers

  • As you can in the above example when the app is loaded the first time, I've added an event called LoadJokeEvent().
  • In our overridden method first of all the onEvent method will be triggered. Which you can confirm in the log.
  • After that, onTransition gets called. As I've said earlier, the onTransition will get called before the onChange. Which contains the currentState, triggered event and nextState.
  • After the completion of onTransition, the onChange method is called. When the joke data is loading the JokeLoadingState passes, after the successful fetch of the data, as you can see in the log we got the JokeLoadedState with the instance JokeModel.

    So the flow is : onEvent > onTransition > onChange > onError

  • Let's quickly change the api to random endpoint for showing the error in order to check if our onError method is working or not.

onError.gif


Creating a Global BlocObserver

  • In the above example we are simply, listening to a particular bloc.
  • Now, let's see how you can observe all the blocs that you've created globally.
  • Create a new file, and then create a class and extends it with the BlocObserver.
  • That's it, now you can override all the methods here as we did in the above example.

    class MyGlobalObserver extends BlocObserver {
    @override
    void onEvent(Bloc bloc, Object? event) {
      super.onEvent(bloc, event);
      debugPrint('${bloc.runtimeType} $event');
    }
    
    @override
    void onChange(BlocBase bloc, Change change) {
      super.onChange(bloc, change);
      debugPrint('${bloc.runtimeType} $change');
    }
    
    @override
    void onTransition(Bloc bloc, Transition transition) {
      super.onTransition(bloc, transition);
      debugPrint('${bloc.runtimeType} $transition');
    }
    
    @override
    void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
      debugPrint('${bloc.runtimeType} $error $stackTrace');
      super.onError(bloc, error, stackTrace);
    }
    }
    
  • It will not work now as we've not attached this class anywhere.
  • Head over to main.dart file and wrap your runApp function around BlocOverrides.runZoned(). And then pass the blocObserver parameter.
    void main() {
    BlocOverrides.runZoned(
        () {
          runApp(const MyApp());
        },
        blocObserver: MyGlobalObserver(),
      );
    }
    
  • You're ready to go now. Now, if a new event is introduced, a new state is emitted, or a bloc error occurs, you can see it immediately within your console and manage it quickly.

Did you find this article valuable?

Support Dhruv Nakum by becoming a sponsor. Any amount is appreciated!

ย