BLoC Pattern - Events & States
The traffic controller for your app's brain
Open interactive version (quiz + challenge)Real-world analogy
What is it?
The BLoC (Business Logic Component) pattern is an architecture for Flutter apps that separates business logic from the UI using events and states. You define Event classes that represent user actions or system triggers, and State classes that represent the current condition of a feature. The BLoC receives events, processes them through business logic, and emits new states that the UI reacts to. Combined with Equatable for efficient comparison, this creates a predictable, testable, and scalable architecture.
Real-world relevance
BLoC is one of the most widely adopted state management solutions in production Flutter apps, used by companies like BMW, Alibaba, and Google teams. The event-state pattern maps naturally to how users interact with apps: the user does something (event), the app processes it (BLoC logic), and the screen updates (state). In team_mvp_kit, every feature follows this pattern with BaseBloc, BaseEvent, and BaseState, making the codebase consistent and onboarding new developers straightforward.
Key points
- What Is BLoC? — BLoC stands for Business Logic Component. It is a design pattern that separates business logic from the UI by using Streams. Events go into the BLoC, and States come out. The UI never contains business logic.
- Event Classes — Events represent user actions or system triggers. Each event is a class. They are the inputs to your BLoC -- the orders the kitchen receives.
- State Classes — States represent the current condition of a feature. The UI reads the current state and renders accordingly. Each state is a class describing what the screen should show.
- Equatable for State Comparison — BLoC uses equality checks to decide whether to rebuild the UI. The Equatable package makes it easy to compare objects by their properties instead of by reference.
- BaseEvent Pattern from team_mvp_kit — In team_mvp_kit, all events extend a common BaseEvent class that extends Equatable. This ensures consistent equality behavior and makes the event hierarchy clean.
- BaseState Pattern from team_mvp_kit — Similarly, all states extend a BaseState class. This provides a unified state hierarchy and ensures Equatable comparison for all states across the app.
- The Bloc Class — A Bloc class takes events as input and emits states as output. You register event handlers using the on method, and each handler processes the event and emits new states.
- Single vs Multi-State BLoCs — Some BLoCs use a single state class with multiple fields (loading, data, error). Others use separate classes per state. team_mvp_kit prefers single-class states with copyWith for simpler features and sealed class hierarchies for complex ones.
- Naming Conventions — Good naming makes BLoC code self-documenting. Events should describe what happened (past tense or requested action). States should describe current conditions.
Code example
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// ---- Base classes (from team_mvp_kit) ----
abstract class BaseEvent extends Equatable {
const BaseEvent();
@override
List<Object?> get props => [];
}
abstract class BaseState extends Equatable {
const BaseState();
@override
List<Object?> get props => [];
}
// ---- Events ----
class LoadTodos extends BaseEvent {}
class AddTodo extends BaseEvent {
final String title;
const AddTodo(this.title);
@override
List<Object?> get props => [title];
}
class ToggleTodo extends BaseEvent {
final int index;
const ToggleTodo(this.index);
@override
List<Object?> get props => [index];
}
class DeleteTodo extends BaseEvent {
final int index;
const DeleteTodo(this.index);
@override
List<Object?> get props => [index];
}
// ---- State ----
class TodoItem {
final String title;
final bool isDone;
const TodoItem({required this.title, this.isDone = false});
TodoItem copyWith({String? title, bool? isDone}) {
return TodoItem(
title: title ?? this.title,
isDone: isDone ?? this.isDone,
);
}
}
class TodoState extends BaseState {
final List<TodoItem> todos;
final bool isLoading;
final String? error;
const TodoState({
this.todos = const [],
this.isLoading = false,
this.error,
});
TodoState copyWith({
List<TodoItem>? todos,
bool? isLoading,
String? error,
}) {
return TodoState(
todos: todos ?? this.todos,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
@override
List<Object?> get props => [todos, isLoading, error];
}
// ---- BLoC ----
class TodoBloc extends Bloc<BaseEvent, TodoState> {
TodoBloc() : super(const TodoState()) {
on<LoadTodos>(_onLoad);
on<AddTodo>(_onAdd);
on<ToggleTodo>(_onToggle);
on<DeleteTodo>(_onDelete);
}
Future<void> _onLoad(
LoadTodos event,
Emitter<TodoState> emit,
) async {
emit(state.copyWith(isLoading: true, error: null));
try {
await Future.delayed(const Duration(seconds: 1));
final todos = [
const TodoItem(title: 'Learn Dart'),
const TodoItem(title: 'Master BLoC'),
const TodoItem(title: 'Build an app'),
];
emit(state.copyWith(todos: todos, isLoading: false));
} catch (e) {
emit(state.copyWith(
isLoading: false,
error: e.toString(),
));
}
}
void _onAdd(AddTodo event, Emitter<TodoState> emit) {
final updated = [
...state.todos,
TodoItem(title: event.title),
];
emit(state.copyWith(todos: updated));
}
void _onToggle(ToggleTodo event, Emitter<TodoState> emit) {
final updated = state.todos.asMap().entries.map((entry) {
if (entry.key == event.index) {
return entry.value.copyWith(isDone: !entry.value.isDone);
}
return entry.value;
}).toList();
emit(state.copyWith(todos: updated));
}
void _onDelete(DeleteTodo event, Emitter<TodoState> emit) {
final updated = [...state.todos]..removeAt(event.index);
emit(state.copyWith(todos: updated));
}
}Line-by-line walkthrough
- 1. Define BaseEvent extending Equatable with an empty props list as the parent of all events.
- 2. Define BaseState extending Equatable as the parent of all state classes.
- 3. LoadTodos event has no data -- it just signals the BLoC to fetch todos.
- 4. AddTodo carries a title string and includes it in props for equality.
- 5. ToggleTodo and DeleteTodo carry an index to identify which todo to act on.
- 6. TodoItem is a simple data class with title and isDone, plus a copyWith method.
- 7. TodoState holds a list of TodoItems, a loading flag, and an optional error string.
- 8. The copyWith method creates a new TodoState, only replacing the fields you specify.
- 9. The props getter lists all three fields so Equatable can compare states accurately.
- 10. TodoBloc extends Bloc with BaseEvent input and TodoState output, starting with an empty state.
- 11. Register four event handlers in the constructor using the on syntax.
- 12. _onLoad emits loading state, simulates an async fetch, then emits loaded state with todos.
- 13. The try-catch ensures that API failures emit an error state instead of crashing.
- 14. _onAdd spreads existing todos and appends a new TodoItem, then emits the updated state.
- 15. _onToggle maps over todos, flipping isDone only for the matching index.
- 16. _onDelete copies the list, removes the item at the given index, and emits.
Spot the bug
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>(_onIncrement);
on<Decrement>(_onDecrement);
}
void _onIncrement(Increment event, Emitter<int> emit) {
state++;
emit(state);
}
void _onDecrement(Decrement event, Emitter<int> emit) {
emit(state - 1);
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- BLoC Library Documentation (bloclibrary.dev)
- Equatable Package (pub.dev)
- Flutter BLoC Concepts (bloclibrary.dev)