STATE MANAGEMENT
Bloc vs Cubit! A fair review
How will each one fit with your purpose
Flutter is awesome! We can build sophisticated visual components and animations quickly. And what that means? That most of our effort is directed to logic and architecture.
Flutter does its work, so let’s discuss ours. We need to tell when repainting the screen or part of it to the framework.
And we can accomplish this with state management tools! I shall present you with two options: Bloc and Cubit.
Courage and effort are not enough without purpose and direction.
— John F. Kennedy
BLoC pattern
These two state management tools are built with the BLoC pattern, so we have to talk about it.
The BLoC pattern implements a reactive behavior. Meaning it has an Observer watching some widget’s state, and as the state changes, the listeners of the observer are invoked.
STREAMS
We can build an Observer on Dart with Streams.
Streams are asynchronous functions that can have multiple returns. To stop it, you have to call the close() function.
Note: You don’t need to call the close function if using BlocProvider Widget.
You provide an event by the input spot (Sink), and each one will be processed and produce a new value that can be listened to on the output spot (stream).
Are Bloc and Cubit streams?
Yes, that is what we have at its core!
Each of them extends o BlocBase class, which has a StreamController inside.
Initially, the Cubit package was forked from Bloc. After, Cubit was integrated into the flutter_bloc package.
First round: the Counter example
The most basic project in Flutter is the Counter app built when you create a new project.
I did a little app with 2 counters pages with the same visual but with a different state management tool.
Let’s see some code but do not forget to add the flutter_bloc package as a dependency (mine is version 8.1.2).
Counter with Bloc
class IncrementEvent {}
class CounterBloc extends Bloc<IncrementEvent, int> {
CounterBloc() : super(0) {
on<IncrementEvent>(_increment);
}
void _increment(IncrementEvent event, Emitter<int> emit) {
emit(state + 1);
}
}
The CounterBloc has to extend the Bloc class. And as a Stream, we need to type our Bloc input and output, so we typed as an IncrementEvent
and an int
, respectively.
In the constructor, we must give an initial value for the stream, which for us is 0. We use the on
function to invoke increment when an IncrementEvent is given.
We emit the current state of the Bloc, which is initially 0, and add 1. And that’s it! We have a Bloc controller.
The page widget.
class _CounterBlocPageState extends State<CounterBlocPage> {
late final CounterBloc _counterBloc;
@override
void initState() {
_counterBloc = CounterBloc();
super.initState();
}
@override
void dispose() {
_counterBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('BLOC'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
BlocBuilder<CounterBloc, int>(
bloc: _counterBloc,
builder: (context, state) {
return Text(
'$state',
style: Theme.of(context).textTheme.headline4,
);
}
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _counterBloc.add(IncrementEvent()),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
For the interface, Bloc and Cubit will be very similar. Just look at how we invoke the increment method by adding an IncrementEvent
on the Bloc.
Counter with Cubit
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
Cubit code is simpler! Even if it has a Stream we do not need to give an input type, just what kind of data it will emit. Don’t forget the initial value of 0.
Page widget
class _CounterCubitPageState extends State<CounterCubitPage> {
late final CounterCubit counterCubit;
@override
void initState() {
counterCubit = CounterCubit();
super.initState();
}
@override
void dispose() {
counterCubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('CUBIT'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
BlocBuilder<CounterCubit, int>(
bloc: counterCubit,
builder: (context, state) {
return Text(
'$state',
style: Theme.of(context).textTheme.headline4,
);
}
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counterCubit.increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
To invoke our increment
function we do it as a normal class, just calling the method with parenthesis.
Comparing Counters
The main differences are:
- Bloc required the input to be typed.
- You invoke the Cubit functions as we do on normal classes.
With that, we can say that Cubit seems better because it has fewer boilerplate and is more readable. Bloc is a more extensive method and as the problem grows in complexity it can be confusing.
What is a real concern here is that codes from tutorials aren’t usually representative when comes to resolving real-world problems.
The Realistic Example
This still being a tutorial code, but I want to make it more realistic.
When controlling a widget’s state we usually have more than one property and at least 3 states Success, Failure, and Loading… Maybe it has a Form. What about a login page? It sounds nice! =)
As our login can have several states, is useful to create a LoginState class to store all information needed.
enum LoginStatus {
idle,
loading,
success,
failure,
}
class LoginState {
final LoginStatus status;
final String? userName;
final String? errorMessage;
LoginState({
required this.status,
required this.userName,
required this.errorMessage,
});
factory LoginState.empty() {
return LoginState(
status: LoginStatus.idle,
userName: null,
errorMessage: null,
);
}
LoginState copyWith({
LoginStatus? status,
String? userName,
String? errorMessage,
}) {
return LoginState(
status: status ?? this.status,
userName: userName ?? this.userName,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
The main states of our app are represented by LoginStatus
class, and I added an idle
status as the initial page condition.
Besides the status, LoginState
also stores the user information (in this case, just the name) and an error message if the logic fails.
I won’t show the page's source code since our goal is state management. But you can check all the source codes from here.
Bloc Login
class LoginEvent {
final String username;
final String password;
const LoginEvent({required this.username, required this.password});
}
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc() : super(LoginState.empty()) {
on<LoginEvent>(_login);
}
Future<void> _login(LoginEvent event, Emitter<LoginState> emit) async {
emit(state.copyWith(status: LoginStatus.loading));
try {
await Future.delayed(const Duration(seconds: 2));
if(event.username != 'admin' || event.password != 'admin') {
throw Exception('Invalid User');
}
emit(state.copyWith(status: LoginStatus.success, userName: 'Felipe'));
} on Exception catch (e) {
emit(state.copyWith(status: LoginStatus.failure, errorMessage: e.toString()));
}
}
}
Cubit Login
class LoginCubit extends Cubit<LoginState> {
LoginCubit() : super(LoginState.empty());
Future<void> login(String username, String password) async {
emit(state.copyWith(status: LoginStatus.loading));
try {
await Future.delayed(const Duration(seconds: 2));
if(username!= 'admin' || password != 'admin') {
throw Exception('Invalid User');
}
emit(state.copyWith(status: LoginStatus.success, userName: 'Felipe'));
} on Exception catch (e) {
emit(state.copyWith(status: LoginStatus.failure, errorMessage: e.toString()));
}
}
}
Comparison
This brings us close to real case problems where the only notable change is the event class with parameters on Bloc.
In a more sophisticated example, we noticed the Bloc’s boilerplate hasn’t increased significantly compared with Cubit.
Also, Bloc is more traceable since it has the extra step of using an event-driven approach.
Conclusion
For general purposes, Cubit seems a more practical tool since it is more readable and requires less code.
There are situations where the app rules should be more tide-up, like in a project with very rotative team members. I recommend Bloc for it.
Other thoughts
What about performance, security, memory usage… and other technical matters? We know Bloc and Cubit have different syntaxes, but implement the same interfaces and are in the same package. So, they shouldn’t have relevant differences in efficiency.
About getting a job… I don’t have data to confirm, but Bloc seems more popular. But if you learn one of them the other wouldn’t be a problem.
Interesting links
If you want to read more of my thoughts and programming tips, then check out my articles and follow or subscribe for upcoming content.