BlocObserver: Debug and Observe your Bloc Easily
Learn how to observe a bloc by simply overriding `onChange`, `onTransition`, `onEvent`, `onError` methods.
Introduction
- What is up Flutter Dev ๐๐ป!!! In this blog, we'll learn about a unique feature that Bloc has to offer.
- I've previously written a few articles about BLoC in the past, covering topics such as what it stands for and how it works, how to get data from an API, how to store bloc locally, and so on.
- So, if you haven't checked it out make sure you check it out first. Or if you have a basic understanding of BLoC you can start right away.
- So, without further ado, let's get started.
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 specificcubit
.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 notifiesBlocObserver.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.
- To track a bloc's error whenever it happens, just override the
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.
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, theonTransition
will get called before theonChange
. Which contains thecurrentState
, triggeredevent
andnextState
. - After the completion of
onTransition
, theonChange
method is called. When the joke data is loading theJokeLoadingState
passes, after the successful fetch of the data, as you can see in the log we got theJokeLoadedState
with the instanceJokeModel
.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.
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 yourrunApp
function aroundBlocOverrides.runZoned()
. And then pass theblocObserver
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.