Flutter Bloc Pattern
Bloc stands for Business Logic Component. The pattern separates what the screen shows from the decisions the app makes. Large teams favor it because every change follows a strict, predictable path. Picture a restaurant: customers place orders, the kitchen prepares meals, and waiters deliver plates. Customers never walk into the kitchen.
The Core Flow
┌────────────┐ Event ┌────────────┐
│ UI │ ─────────→ │ Bloc │
│ (customer) │ │ (kitchen) │
│ │ ←───────── │ │
└────────────┘ State └────────────┘
- Event: something the user did, such as "increment pressed".
- Bloc: the logic that receives events and decides what happens.
- State: the new snapshot of data the UI displays.
Installing the Package
flutter pub add flutter_bloc
Cubit: The Simple Version
A Cubit skips events. The UI calls methods directly, and each method emits a new state. Start with Cubit for most features.
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() {
emit(state + 1);
}
void decrement() {
emit(state - 1);
}
}
The number inside super(0) is the starting state. The emit function publishes a new state, and the UI updates.
Providing a Cubit to the Tree
BlocProvider creates the Cubit and makes it available to all widgets below.
BlocProvider(
create: (context) => CounterCubit(),
child: CounterPage(),
)
Showing State with BlocBuilder
BlocBuilder rebuilds its child whenever the state changes.
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) {
return Text('Count: $count', style: TextStyle(fontSize: 32));
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<CounterCubit>().increment();
},
child: Icon(Icons.add),
),
);
}
}
The call context.read finds the Cubit and calls a method without rebuilding the button.
Bloc: The Event-Driven Version
A full Bloc receives event objects. Events leave a record of everything that happened, which helps with debugging and analytics.
abstract class CounterEvent {}
class IncrementPressed extends CounterEvent {}
class DecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementPressed>((event, emit) {
emit(state + 1);
});
on<DecrementPressed>((event, emit) {
emit(state - 1);
});
}
}
// Sending an event
context.read<CounterBloc>().add(IncrementPressed());
Cubit or Bloc
| Feature | Cubit | Bloc |
|---|---|---|
| Input | Method calls | Event objects |
| Code amount | Small | Larger |
| Event history | No | Yes |
| Best for | Simple features | Complex flows, event tracking |
Modeling Several States
Real screens move through loading, success, and failure. Create one class per situation and let the UI react to each.
abstract class WeatherState {}
class WeatherLoading extends WeatherState {}
class WeatherLoaded extends WeatherState {
final double temperature;
WeatherLoaded(this.temperature);
}
class WeatherError extends WeatherState {}
BlocBuilder<WeatherCubit, WeatherState>(
builder: (context, state) {
if (state is WeatherLoading) {
return CircularProgressIndicator();
}
if (state is WeatherLoaded) {
return Text('${state.temperature} degrees');
}
return Text('Could not load weather');
},
)
BlocListener for One-Time Actions
Some reactions happen once: showing a snack bar, opening a new screen. BlocListener runs code on a state change without rebuilding any widget.
BlocListener<WeatherCubit, WeatherState>(
listener: (context, state) {
if (state is WeatherError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Network problem')),
);
}
},
child: WeatherView(),
)
Use BlocConsumer to combine a builder and a listener in one widget.
Folder Layout
lib/features/weather/
├── bloc/
│ ├── weather_cubit.dart
│ └── weather_state.dart
├── data/
│ └── weather_repository.dart
└── view/
└── weather_page.dart
This layout keeps UI, logic, and data code in separate rooms. Each room changes without disturbing the others.
Good Habits
- Keep states immutable: emit a new object instead of editing the old one.
- Keep widgets free of business rules.
- Write tests for Cubits, which need no UI to run.
Why Teams Choose Bloc
Bloc enforces one direction of data flow. Events go in, states come out, and nothing else changes the screen. New developers follow the same path in every feature, which makes large codebases easier to maintain. The pattern lets teams write tests that feed events to a Bloc and check the states that follow, with no screen involved.
Practice Ideas
Build a login form with a Cubit that emits idle, loading, success, and failure states. Use a BlocBuilder to disable the button while loading and a BlocListener to open the home screen on success. Replace the Cubit with a Bloc and compare the amount of code each version needs.
Testing a Bloc
Bloc code tests cleanly because input and output are plain objects. The bloc_test package lets a test describe a starting state, send events, and list the states that should follow. The test needs no widgets, no emulator, and no network. A suite of such tests documents how each feature behaves and protects it against accidental changes during later edits.
Observing Every Change
Flutter Bloc offers a global observer that reports every event, every state change, and every error in the app. Print these messages during development to trace exactly how data moved before a bug appeared. Production builds can forward the same reports to a logging service. This traceability is a big reason teams trust the pattern for large products.
