| name | bloc |
| description | Best practices for Bloc state management in Flutter/Dart. |
| when_to_use | Use when writing, modifying, or reviewing code that uses package:bloc, package:flutter_bloc, or package:bloc_test. |
| allowed-tools | Read Glob Grep |
Bloc
State management library for Dart and Flutter using the BLoC (Business Logic Component) pattern to separate business logic from the presentation layer.
Core Standards
Apply these standards to ALL Bloc/Cubit work:
- Use
blocTest() from package:bloc_test for all Bloc and Cubit tests — never raw test() with manual stream assertions
- Use
package:mocktail for mocking — never package:mockito
- No bloc-to-bloc direct dependencies — blocs communicate through the UI or shared repositories
- Page/View separation — Page provides the Bloc/Cubit via
BlocProvider, View consumes via BlocBuilder/BlocListener
- Sealed classes for events and multi-state types — enables exhaustive pattern matching with Dart 3
switch
- Equatable for all states and events — extend
Equatable and override props for value equality
- Business logic in Bloc/Cubit only — never in widgets, pages, or views
- Single responsibility — one Bloc/Cubit per feature concern
- Emit only after async checks — use
emit only inside the handler callback
Cubit vs Bloc
| Aspect | Cubit | Bloc |
|---|
| API | Functions → emit(state) | Events → on<Event> → emit(state) |
| Complexity | Low | Higher |
| Traceability | Less (no event log) | Full (events + transitions) |
| When to use | Simple state, UI-driven logic | Complex flows, event-driven, transforms |
| Testing | Call methods, assert states | Add events, assert states |
Cubit Example
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
Bloc Example
sealed class CounterEvent extends Equatable {
const CounterEvent();
@override
List<Object> get props => [];
}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
on<CounterDecrementPressed>((event, emit) => emit(state - 1));
}
}
Naming Conventions
Events
Pattern: BlocSubject + Noun + VerbPastTense
| Event class name | Meaning |
|---|
TodoListSubscriptionRequested | Subscribing to todo list stream |
TodoListTodoDeleted | Deleting a specific todo |
TodoListUndoDeletionRequested | Undoing the last deletion |
LoginFormSubmitted | Submitting the login form |
ProfilePageRefreshed | Refreshing the profile page |
sealed class TodoListEvent extends Equatable {
const TodoListEvent();
@override
List<Object> get props => [];
}
final class TodoListSubscriptionRequested extends TodoListEvent {}
final class TodoListTodoDeleted extends TodoListEvent {
const TodoListTodoDeleted({required this.todo});
final Todo todo;
@override
List<Object> get props => [todo];
}
States
Subclass Approach (multiple state types)
Use when each state carries different data.
| State class name | Meaning |
|---|
LoginInitial | No action taken yet |
LoginInProgress | Login request in flight |
LoginSuccess | Login succeeded |
LoginFailure | Login failed |
sealed class LoginState extends Equatable {
const LoginState();
@override
List<Object> get props => [];
}
final class LoginInitial extends LoginState {}
final class LoginInProgress extends LoginState {}
final class LoginSuccess extends LoginState {
const LoginSuccess({required this.user});
final User user;
@override
List<Object> get props => [user];
}
final class LoginFailure extends LoginState {
const LoginFailure({required this.error});
final String error;
@override
List<Object> get props => [error];
}
Single Class Approach (one state, multiple fields)
Use when all states share the same data shape.
| Field | Type | Purpose |
|---|
status | enum | Current loading status |
items | List<Item> | Loaded data |
error | String? | Error message if failed |
enum TodoListStatus { initial, loading, success, failure }
class TodoListState extends Equatable {
const TodoListState({
this.status = TodoListStatus.initial,
this.todos = const [],
this.error,
});
final TodoListStatus status;
final List<Todo> todos;
final String? error;
TodoListState copyWith({
TodoListStatus? status,
List<Todo>? todos,
String? error,
}) {
return TodoListState(
status: status ?? this.status,
todos: todos ?? this.todos,
error: error ?? this.error,
);
}
@override
List<Object?> get props => [status, todos, error];
}
Architecture
| Layer | Contains | Depends on |
|---|
| Presentation | Pages, Views, Widgets | Business Logic |
| Business Logic | Blocs, Cubits | Data |
| Data | Repositories, Data Providers | External sources |
Data Layer
Repositories abstract data sources and provide a clean API for Blocs/Cubits. Mirror the feature folder structure under test/ for all test files.
See references/architecture.md for the repository example, feature folder structure, and test directory layout.
Flutter Widgets
BlocProvider — creates and provides a Bloc/Cubit to the subtree
BlocBuilder — rebuilds widget when state changes
BlocListener — executes side effects (navigation, snackbar) on state change
BlocConsumer — combines BlocBuilder + BlocListener
BlocSelector — rebuilds only when a selected property changes
- Use
context.read in callbacks (onPressed, onTap), context.watch or BlocBuilder in build methods
- Never use
context.watch outside of build
See references/widgets.md for the full widget and context extension tables, Page/View pattern example, and BlocListener example. See references/testing.md for blocTest() parameters, Cubit/Bloc test examples, mocking dependencies, and widget integration tests. See references/patterns.md for adding features with Bloc/Cubit, async operations, and event transformers.