The BLoC Pattern: Core Concepts
The BLoC Pattern: Core Concepts
The Business Logic Component (BLoC) pattern is an architectural state management pattern developed by Google for Flutter. Its central philosophy is deceptively simple: Events go in, States come out. The BLoC sits between your UI layer and your data layer, receiving user-driven or system-driven events, processing business logic, and emitting new states that the UI reacts to. This creates a strict, testable boundary between presentation and logic.
flutter_bloc package (by Felix Angelov) builds on top of the core concept and is the most widely adopted implementation in the community.The Core Data Flow
Understanding BLoC starts with its unidirectional data flow. There are three key participants:
- Event — an immutable object that represents something that happened (e.g., a button press, a page load, a search query submitted). Events are the inputs to a BLoC.
- BLoC — the component that receives events and, after processing business logic (validation, API calls, transformations), emits new states.
- State — an immutable object that describes the current condition of a feature (e.g., loading, loaded with data, error). States are the outputs of a BLoC.
The UI dispatches events to the BLoC and listens to state changes. Crucially, the UI never contains business logic, and the BLoC never holds a reference to UI widgets.
Defining Events and States
// --- Events (inputs to the BLoC) ---
abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}
class CounterResetPressed extends CounterEvent {}
// --- States (outputs from the BLoC) ---
class CounterState {
final int count;
const CounterState(this.count);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is CounterState && other.count == count);
@override
int get hashCode => count.hashCode;
}
Implementing a BLoC
With the flutter_bloc package, a BLoC extends Bloc<Event, State>. You define an initial state in the constructor and register event handlers using on<EventType>(handler). Each handler receives the event and an Emitter that you call to push new states downstream.
A Complete Counter BLoC
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<CounterIncrementPressed>(_onIncrement);
on<CounterDecrementPressed>(_onDecrement);
on<CounterResetPressed>(_onReset);
}
void _onIncrement(
CounterIncrementPressed event,
Emitter<CounterState> emit,
) {
emit(CounterState(state.count + 1));
}
void _onDecrement(
CounterDecrementPressed event,
Emitter<CounterState> emit,
) {
// Business logic: prevent going below zero
if (state.count > 0) {
emit(CounterState(state.count - 1));
}
}
void _onReset(
CounterResetPressed event,
Emitter<CounterState> emit,
) {
emit(const CounterState(0));
}
}
Connecting the BLoC to the UI
The flutter_bloc package provides two key widgets for connecting your BLoC to the widget tree:
BlocProvider— creates and provides the BLoC to its subtree via the widget tree (InheritedWidget under the hood). It also handles closing the BLoC when the widget is removed.BlocBuilder— rebuilds its subtree whenever the BLoC emits a new state. It only rebuilds when the state actually changes (based on equality).
Wiring BLoC to the Widget Tree
// Provide the BLoC above the widget that needs it
BlocProvider(
create: (context) => CounterBloc(),
child: CounterPage(),
)
// Inside CounterPage — read and react to state
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BLoC Counter')),
body: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Center(
child: Text(
'Count: ${state.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
);
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'inc',
onPressed: () =>
context.read<CounterBloc>().add(CounterIncrementPressed()),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'dec',
onPressed: () =>
context.read<CounterBloc>().add(CounterDecrementPressed()),
child: const Icon(Icons.remove),
),
],
),
);
}
}
Why BLoC Enforces a UI/Logic Boundary
The BLoC pattern enforces separation of concerns through its design constraints:
- The BLoC receives only plain Dart objects (events) and emits only plain Dart objects (states). It has zero dependency on Flutter widgets or
BuildContext. - Because the BLoC is pure Dart, it can be unit-tested without spinning up a widget tree. You feed events in and assert on the emitted states.
- The UI layer is a thin shell — it maps user gestures to events and maps states to widgets. No
if/elsebusiness logic should live inbuild()methods. - Multiple widgets can observe the same BLoC state independently, enabling fine-grained rebuilds without prop-drilling.
flutter_bloc uses equality checks — if two successive states are equal, the BLoC will not emit the second one and BlocBuilder will not rebuild. Always ensure your state classes correctly implement == and hashCode, or use the equatable package.Summary
The BLoC pattern structures state management around three concepts: Events (inputs), BLoC (processor), and States (outputs). Data flows in one direction — the UI dispatches events, the BLoC handles them and emits states, and the UI rebuilds in response. This separation makes large Flutter applications easier to reason about, test, and maintain. In the next lesson you will explore Cubit, a simplified variant of BLoC that replaces events with direct method calls.