When building Flutter apps, managing state is one of the most important decisions. State simply means how data looks and behaves at a given point in your app. BLoC (Business Logic Component) is a popular state management solution in Flutter that helps you separate your app into three clear layers:
- Data layer – managing raw data (like APIs, databases).
- Business logic layer – deciding what happens to the data (transformations, events, decisions).
- Presentation layer – the user interface that displays the data.
This separation makes code easier to test, maintain, and scale as your app grows.
Key Concepts in BLoC
To understand BLoC better, let’s look at the core concepts:
- Streams:
Streams work like pipelines for data. They allow data to flow asynchronously. In BLoC, events and states are sent through streams so widgets can react whenever data changes. - Cubit:
A Cubit is the simplest form of state management in Flutter Bloc. It doesn’t use events but exposes functions that directly emit new states. For less complex apps, Cubit is easier to use and understand. - Bloc:
Unlike Cubit, BLoC works with events and states. You send an event (like a button press) into the Bloc, and based on the business logic, it outputs a new state. This makes it powerful for handling more complex user interactions and app flows.
What is Streams?
- A stream is a sequence of asynchronous data. Think of it like a water pipe with water(asynchronous data) flowing through it.
- Simple example of stream using async generator:
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}
- To consume this stream and find the sum of all elements in the stream, we can create a function sumStream like this :
Future<int> sumStream(Stream<int> stream) async {
int sum = 0;
await for (int value in stream) {
sum += value;
}
return sum;
}
- And to get the sum of all elements in stream, we can call it :
void main() async {
/// Initialize a stream of integers 0-9
Stream<int> stream = countStream(10);
/// Compute the sum of the stream of integers
int sum = await sumStream(stream);
/// Print the sum
print(sum); // 45
}
What is Cubit?
- A Cubit exposes functions that can be called to trigger state changes.
- Cubit outputs states which are part of UI, and on change of this stat,e the UI components gets notified and redrawn.

Simple cubit
- Creating counter cubit:
class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);
}
- You can emit a new update state from Cubit as follows:
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
- The emit method with Cubit is protected, meaning it should only be used inside of a Cubit.
- Now to use this cubit
void main() {
final cubit = CounterCubit();
print(cubit.state); // 0
cubit.increment();
print(cubit.state); // 1
cubit.close();
}
Stream with cubit
- Cubit exposes a Stream which allows us to receive real-time state updates:
Future<void> main() async {
final cubit = CounterCubit();
final subscription = cubit.stream.listen(print); // 1
cubit.increment();
await Future.delayed(Duration.zero);
await subscription.cancel();
await cubit.close();
}
Observing a Cubit
- We can observe all the state changes in a Cubit by overriding the onChange method inherited from the Cubit class.
- A Change occurs just before the state of the Cubit is updated. A Change consists of the current state and the next state.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
}
void main() {
CounterCubit()
..increment()
..close();
}
// this will output Change { currentState: 0, nextState: 1 }
BlocObserver
- It can access changes to all the cubits in our app.
- To create a blocObserver, all we need to do is extend the blocObserver and override the onChange method.
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
}
- In order to use it, just initialise it :
void main() {
Bloc.observer = SimpleBlocObserver();
CounterCubit()
..increment()
..close();
}
This will output :
CounterCubit Change { currentState: 0, nextState: 1 }
Change { currentState: 0, nextState: 1 }
- In BlocObserver, we have access to the Cubit instance in addition to the Change itself.
Cubit Error Handling
- Every Cubit has an addError method, which can be used to indicate that an error has occurred.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() {
addError(Exception('increment error!'), StackTrace.current);
emit(state + 1);
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
- onError can also be overridden in BlocObserver to handle all reported errors globally.
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) {
print('${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}
}
What is Bloc?

- Bloc is similar to Cubit but instead of calling a function,it relies on events.
- Events are the input to a Bloc. They are commonly introduced in response to user activities, such as button clicks, or lifecycle events, such as page loads.
- The shift from one state to another is called a transition.
- Bloc requires us to register event handlers via the on API, in contrast to Cubit functions. An event handler is responsible for converting any incoming events into zero or more outgoing states.
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) {
emit(state + 1);
});
}
}
- Usage :
Future<void> main() async {
final bloc = CounterBloc();
print(bloc.state); // 0
bloc.add(CounterIncrementPressed());
await Future.delayed(Duration.zero);
print(bloc.state); // 1
await bloc.close();
}
Using Stream
Future<void> main() async {
final bloc = CounterBloc();
final subscription = bloc.stream.listen(print); // 1
bloc.add(CounterIncrementPressed());
await Future.delayed(Duration.zero);
await subscription.cancel();
await bloc.close();
}
Observing a Bloc
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
}
void main() {
CounterBloc()
..add(CounterIncrementPressed())
..close();
} //This will print Change { currentState: 0, nextState: 1 }
- In Bloc, we can know what triggered the event by overriding onTransition, as it is event-driven, but that is not possible in cubit.
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
super.onTransition(transition);
print(transition);
}
} // This will print Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
- Another unique of Bloc is that we can override the onEvent method, which gets called whenever a new event is added.onEvent is called as soon as the event is added.
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onEvent(CounterEvent event) {
super.onEvent(event);
print(event);
}
}
This will print Instance of 'CounterIncrementPressed'
- BlocObserver
- Similar to Cubit, we can override the onChange and onError globally to observe all the blocs.
- Error handling
- Same as cubit.
Reference : https://bloclibrary.dev/bloc-concepts/#bloc