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()frompackage:bloc_testfor all Bloc and Cubit tests — never rawtest()with manual stream assertions - Use
package:mocktailfor mocking — neverpackage:mockito build:constructs the bloc under test — return a new instance from the callback, never one built insetUpand shared, so everyblocTeststarts from a clean bloc and its lifecycle stays withblocTest- 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 viaBlocBuilder/BlocListener - Sealed classes for events and multi-state types — enables exhaustive pattern matching with Dart 3
switch - Equatable for all states and events — extend
Equatableand overridepropsfor value equality - Business logic in Bloc/Cubit only — never in widgets, pages, or views
- Single responsibility — one Bloc/Cubit per feature concern
emitonly inside the handler — callemitfrom theon<Event>callback or Cubit method that owns it, never from a stream subscription or callback that outlives the handler- Dart 3.13 primary constructors — on a Dart 3.13+ baseline, declare event, state, and widget fields as primary-constructor declaring parameters (
class const ProfileRequested(final String userId) extends ProfileEvent) rather thanthis.field; keep the classic form only below 3.13
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 const TodoListTodoDeleted({required final Todo todo})
extends TodoListEvent {
@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 const LoginSuccess({required final User user}) extends LoginState {
@override
List<Object> get props => [user];
}
final class const LoginFailure({required final String error})
extends LoginState {
@override
List<Object> get props => [error];
}
Single Class Approach (one state, multiple fields)
Use when all states share the same data shape: one class holding a status enum plus the
data fields, with a copyWith for transitions. See
references/patterns.md for the full shape.
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
Use BlocProvider to supply a Bloc or Cubit to a subtree, then BlocBuilder,
BlocListener, BlocConsumer, or BlocSelector to consume it. Reach for BlocSelector
when a rebuild should depend on one field rather than the whole state, and BlocListener
for side effects such as navigation or a snackbar.
The rule that matters: context.read in callbacks (onPressed, onTap),
context.watch or BlocBuilder in build. Never call context.watch outside a
build method.
Additional Resources
- references/architecture.md — repository example, feature folder structure, test directory layout
- references/widgets.md — widget and context extension tables, Page/View pattern,
BlocListenerexample - references/testing.md —
blocTest()parameters, Cubit/Bloc test examples, mocking dependencies, widget integration tests - references/patterns.md — single-class state, adding features with Bloc/Cubit, async operations, event transformers