vgv-bloc
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()frompackage:bloc_testfor all Bloc and Cubit tests — never rawtest()with manual stream assertions - Use
package:mocktailfor mocking — neverpackage: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 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
- Emit only after async checks — use
emitonly 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();
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();
List<Object> get props => [];
}
final class TodoListSubscriptionRequested extends TodoListEvent {}
final class TodoListTodoDeleted extends TodoListEvent {
const TodoListTodoDeleted({required this.todo});
final Todo todo;
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();
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;
List<Object> get props => [user];
}
final class LoginFailure extends LoginState {
const LoginFailure({required this.error});
final String error;
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,
);
}
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 subtreeBlocBuilder— rebuilds widget when state changesBlocListener— executes side effects (navigation, snackbar) on state changeBlocConsumer— combinesBlocBuilder+BlocListenerBlocSelector— rebuilds only when a selected property changes- Use
context.readin callbacks (onPressed,onTap),context.watchorBlocBuilderinbuildmethods - Never use
context.watchoutside ofbuild
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.