bloc
Use when creating a Cubit or Bloc, modeling state with sealed classes or status enums, wiring BlocBuilder/BlocListener/BlocProvider, writing bloc tests, or choosing between Cubit and Bloc.
Install
npx skills add https://github.com/evanca/flutter-ai-rules/tree/main/skills/bloc
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install evanca-flutter-ai-rules@llmmart
git clone https://github.com/evanca/flutter-ai-rules.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole evanca/flutter-ai-rules collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Bloc Skill
Design, implement, and test state management using the bloc and flutter_bloc libraries.
When to Use
Use this skill when:
- Creating a new Cubit or Bloc for a feature.
- Modeling state (choosing between sealed classes and a single state class with status enum).
- Wiring
BlocBuilder,BlocListener,BlocConsumer, orBlocProviderin the widget tree. - Writing unit tests for a Cubit or Bloc.
- Deciding between Cubit and Bloc.
- Refactoring existing state management to follow bloc conventions.
1. Cubit vs Bloc
| Situation | Use |
|---|---|
| Simple state, no events needed | Cubit |
| Complex flows, event traceability needed | Bloc |
| Advanced event processing (debounce, throttle) | Bloc with event transformers |
Default to Cubit. Refactor to Bloc only when requirements grow.
2. Naming Conventions
Events (Bloc only)
- Named in past tense:
LoginButtonPressed,UserProfileLoaded. - Format:
BlocSubject+ optional noun + verb. - Initial load event:
BlocSubjectStarted(e.g.,AuthenticationStarted). - Base event class:
BlocSubjectEvent.
States
- Named as nouns (states are snapshots in time).
- Base state class:
BlocSubjectState. - Sealed subclasses:
BlocSubject+Initial|InProgress|Success|Failure.- Example:
LoginInitial,LoginInProgress,LoginSuccess,LoginFailure.
- Example:
- Single-class approach:
BlocSubjectState+BlocSubjectStatusenum (initial,loading,success,failure).
3. Modeling State
When to use a sealed class with subclasses
- States are well-defined and mutually exclusive.
- Type-safe exhaustive
switchis desired. - Subclass-specific properties exist.
@immutable
sealed class LoginState extends Equatable {
const LoginState();
}
final class LoginInitial extends LoginState {
@override
List<Object?> get props => [];
}
final class LoginInProgress extends LoginState {
@override
List<Object?> get props => [];
}
final class LoginSuccess extends LoginState {
const LoginSuccess(this.user);
final User user;
@override
List<Object?> get props => [user];
}
final class LoginFailure extends LoginState {
const LoginFailure(this.message);
final String message;
@override
List<Object?> get props => [message];
}
Handle all states exhaustively in the UI:
switch (state) {
case LoginInitial(): ...
case LoginInProgress(): ...
case LoginSuccess(:final user): ...
case LoginFailure(:final message): ...
}
When to use a single class with a status enum
- Many shared properties across states.
- Simpler, more flexible; previous data must be retained after failure.
enum LoginStatus { initial, loading, success, failure }
@immutable
class LoginState extends Equatable {
const LoginState({
this.status = LoginStatus.initial,
this.user,
this.errorMessage,
});
final LoginStatus status;
final User? user;
final String? errorMessage;
LoginState copyWith({
LoginStatus? status,
User? user,
String? errorMessage,
}) {
return LoginState(
status: status ?? this.status,
user: user ?? this.user,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, user, errorMessage];
}
State rules (both approaches)
- Extend
Equatableand pass all relevant fields toprops. - Copy
List/Mapproperties withList.of/Map.ofinsideprops. - Annotate with
@immutable. - Always emit a new instance; never reuse the same state object.
- Duplicate states are ignored by bloc — ensure meaningful state changes.
4. Cubit Implementation
class LoginCubit extends Cubit<LoginState> {
LoginCubit(this._authRepository) : super(const LoginState());
final AuthRepository _authRepository;
Future<void> login(String email, String password) async {
emit(state.copyWith(status: LoginStatus.loading));
try {
final user = await _authRepository.login(email, password);
emit(state.copyWith(status: LoginStatus.success, user: user));
} catch (e) {
emit(state.copyWith(status: LoginStatus.failure, errorMessage: e.toString()));
}
}
}
Rules:
- Only call
emitinside the Cubit/Bloc. - Public methods return
voidorFuture<void>only. - Keep business logic out of UI.
- When overriding
storagein aHydratedCubit, pass it as a named parameter:super(initialState, storage: storage).
5. Bloc Implementation
sealed class LoginEvent {}
final class LoginSubmitted extends LoginEvent {
LoginSubmitted({required this.email, required this.password});
final String email;
final String password;
}
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc(this._authRepository) : super(LoginInitial()) {
on<LoginSubmitted>(_onLoginSubmitted);
}
final AuthRepository _authRepository;
Future<void> _onLoginSubmitted(
LoginSubmitted event,
Emitter<LoginState> emit,
) async {
emit(LoginInProgress());
try {
final user = await _authRepository.login(event.email, event.password);
emit(LoginSuccess(user));
} catch (e) {
emit(LoginFailure(e.toString()));
}
}
}
Rules:
- Trigger state changes via
bloc.add(Event()), not custom public methods. - Keep event handler methods private (
_onEventName). - Internal/repository events must be private and may use custom transformers.
6. Architecture
Three layers — each must stay in its own boundary:
Presentation → Business Logic (Cubit/Bloc) → Data (Repository → DataProvider)
- Data Layer: Repositories wrap data providers. Providers perform raw CRUD (HTTP, DB). Repositories expose clean domain objects.
- Business Logic Layer: Cubits/Blocs receive repository data and emit states. Inject repositories via constructor.
- Presentation Layer: Renders UI based on state. Handles user input by calling cubit methods or adding bloc events.
Rules:
- Blocs must not access data providers directly — only via repositories.
- No direct bloc-to-bloc communication. Use
BlocListenerin the UI to bridge blocs. - For shared data, inject the same repository into multiple blocs.
- Initialize
BlocObserverinmain.dart.
7. Flutter Bloc Widgets
| Widget | Use |
|---|---|
BlocProvider |
Provide a bloc to a subtree |
MultiBlocProvider |
Provide multiple blocs without nesting |
BlocBuilder |
Rebuild UI on state change |
BlocListener |
Side effects only (navigation, dialogs, snackbars) |
MultiBlocListener |
Listen to multiple blocs without nesting |
BlocConsumer |
Rebuild UI + side effects together |
BlocSelector |
Rebuild only when a selected slice of state changes |
RepositoryProvider |
Provide a repository to the widget tree |
MultiRepositoryProvider |
Provide multiple repositories without nesting |
BlocProvider(
create: (context) => LoginCubit(context.read<AuthRepository>()),
child: LoginView(),
);
BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) {
return switch (state.status) {
LoginStatus.loading => const CircularProgressIndicator(),
LoginStatus.success => const HomeView(),
LoginStatus.failure => Text(state.errorMessage ?? 'Error'),
LoginStatus.initial => const LoginForm(),
};
},
);
BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state.status == LoginStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Login failed')),
);
}
},
child: LoginForm(),
);
Rules:
- Use
context.read<T>()in callbacks (not inbuild). - Use
context.watch<T>()inbuildonly when necessary; preferBlocBuilder. - Never call
context.watchorcontext.selectat the root ofbuild— scope withBuilder. - Handle all possible states in the UI (initial, loading, success, failure).
8. Testing
Use bloc_test package. Mock repositories with mocktail.
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockAuthRepository extends Mock implements AuthRepository {}
void main() {
group('LoginCubit', () {
late AuthRepository authRepository;
late LoginCubit loginCubit;
setUp(() {
authRepository = MockAuthRepository();
loginCubit = LoginCubit(authRepository);
});
tearDown(() => loginCubit.close());
test('initial state should be LoginState with status initial', () {
expect(loginCubit.state, const LoginState());
});
blocTest<LoginCubit, LoginState>(
'should emit [loading, success] when login succeeds',
build: () {
when(() => authRepository.login(any(), any()))
.thenAnswer((_) async => fakeUser);
return loginCubit;
},
act: (cubit) => cubit.login('email@test.com', 'password'),
expect: () => [
const LoginState(status: LoginStatus.loading),
LoginState(status: LoginStatus.success, user: fakeUser),
],
);
blocTest<LoginCubit, LoginState>(
'should emit [loading, failure] when login throws',
build: () {
when(() => authRepository.login(any(), any()))
.thenThrow(Exception('error'));
return loginCubit;
},
act: (cubit) => cubit.login('email@test.com', 'wrong'),
expect: () => [
const LoginState(status: LoginStatus.loading),
isA<LoginState>().having((s) => s.status, 'status', LoginStatus.failure),
],
);
});
}
Rules:
- Always call
tearDown(() => cubit.close()). - Use
blocTestfor state emission assertions. - Use
group()named after the class under test. - Name test cases with "should" to describe expected behavior.
- Register fallback values for custom types:
registerFallbackValue(MyEvent()).
9. Common Pitfalls
| Pitfall | Fix |
|---|---|
| Emitting the same state instance twice | Always create a new state object; bloc ignores duplicate emissions via ==. |
Calling context.watch inside callbacks |
Use context.read in callbacks; watch is only valid inside build. |
Forgetting Equatable props |
Add every field to props; missing fields cause silent state update bugs. |
| Mutable state fields | Keep state @immutable; use copyWith or new sealed subclass instances. |
| Business logic in widgets | Move all logic into the Cubit/Bloc; widgets only dispatch events or call methods. |
// BAD — mutating state in-place
state.items.add(newItem);
emit(state);
// GOOD — emit a new state with copied list
emit(state.copyWith(items: [...state.items, newItem]));
References
Files (flutter-ai-rules)
-
SKILL.md 10.9 KB
--- name: bloc description: "Use when creating a Cubit or Bloc, modeling state with sealed classes or status enums, wiring BlocBuilder/BlocListener/BlocProvider, writing bloc tests, or choosing between Cubit and Bloc." license: MIT --- # Bloc Skill Design, implement, and test state management using the [bloc](https://pub.dev/packages/bloc) and [flutter_bloc](https://pub.dev/packages/flutter_bloc) libraries. ## When to Use Use this skill when: * Creating a new Cubit or Bloc for a feature. * Modeling state (choosing between sealed classes and a single state class with status enum). * Wiring `BlocBuilder`, `BlocListener`, `BlocConsumer`, or `BlocProvider` in the widget tree. * Writing unit tests for a Cubit or Bloc. * Deciding between Cubit and Bloc. * Refactoring existing state management to follow bloc conventions. --- ## 1. Cubit vs Bloc | Situation | Use | |---|---| | Simple state, no events needed | `Cubit` | | Complex flows, event traceability needed | `Bloc` | | Advanced event processing (debounce, throttle) | `Bloc` with event transformers | **Default to `Cubit`. Refactor to `Bloc` only when requirements grow.** --- ## 2. Naming Conventions ### Events (Bloc only) - Named in **past tense**: `LoginButtonPressed`, `UserProfileLoaded`. - Format: `BlocSubject` + optional noun + verb. - Initial load event: `BlocSubjectStarted` (e.g., `AuthenticationStarted`). - Base event class: `BlocSubjectEvent`. ### States - Named as **nouns** (states are snapshots in time). - Base state class: `BlocSubjectState`. - Sealed subclasses: `BlocSubject` + `Initial` | `InProgress` | `Success` | `Failure`. - Example: `LoginInitial`, `LoginInProgress`, `LoginSuccess`, `LoginFailure`. - Single-class approach: `BlocSubjectState` + `BlocSubjectStatus` enum (`initial`, `loading`, `success`, `failure`). --- ## 3. Modeling State ### When to use a sealed class with subclasses - States are **well-defined and mutually exclusive**. - Type-safe exhaustive `switch` is desired. - Subclass-specific properties exist. ```dart @immutable sealed class LoginState extends Equatable { const LoginState(); } final class LoginInitial extends LoginState { @override List<Object?> get props => []; } final class LoginInProgress extends LoginState { @override List<Object?> get props => []; } final class LoginSuccess extends LoginState { const LoginSuccess(this.user); final User user; @override List<Object?> get props => [user]; } final class LoginFailure extends LoginState { const LoginFailure(this.message); final String message; @override List<Object?> get props => [message]; } ``` Handle all states exhaustively in the UI: ```dart switch (state) { case LoginInitial(): ... case LoginInProgress(): ... case LoginSuccess(:final user): ... case LoginFailure(:final message): ... } ``` ### When to use a single class with a status enum - Many shared properties across states. - Simpler, more flexible; previous data must be retained after failure. ```dart enum LoginStatus { initial, loading, success, failure } @immutable class LoginState extends Equatable { const LoginState({ this.status = LoginStatus.initial, this.user, this.errorMessage, }); final LoginStatus status; final User? user; final String? errorMessage; LoginState copyWith({ LoginStatus? status, User? user, String? errorMessage, }) { return LoginState( status: status ?? this.status, user: user ?? this.user, errorMessage: errorMessage ?? this.errorMessage, ); } @override List<Object?> get props => [status, user, errorMessage]; } ``` ### State rules (both approaches) - Extend `Equatable` and pass all relevant fields to `props`. - Copy `List`/`Map` properties with `List.of`/`Map.of` inside `props`. - Annotate with `@immutable`. - Always emit a **new instance**; never reuse the same state object. - Duplicate states are ignored by bloc — ensure meaningful state changes. --- ## 4. Cubit Implementation ```dart class LoginCubit extends Cubit<LoginState> { LoginCubit(this._authRepository) : super(const LoginState()); final AuthRepository _authRepository; Future<void> login(String email, String password) async { emit(state.copyWith(status: LoginStatus.loading)); try { final user = await _authRepository.login(email, password); emit(state.copyWith(status: LoginStatus.success, user: user)); } catch (e) { emit(state.copyWith(status: LoginStatus.failure, errorMessage: e.toString())); } } } ``` Rules: - Only call `emit` inside the Cubit/Bloc. - Public methods return `void` or `Future<void>` only. - Keep business logic out of UI. - When overriding `storage` in a `HydratedCubit`, pass it as a named parameter: `super(initialState, storage: storage)`. --- ## 5. Bloc Implementation ```dart sealed class LoginEvent {} final class LoginSubmitted extends LoginEvent { LoginSubmitted({required this.email, required this.password}); final String email; final String password; } class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc(this._authRepository) : super(LoginInitial()) { on<LoginSubmitted>(_onLoginSubmitted); } final AuthRepository _authRepository; Future<void> _onLoginSubmitted( LoginSubmitted event, Emitter<LoginState> emit, ) async { emit(LoginInProgress()); try { final user = await _authRepository.login(event.email, event.password); emit(LoginSuccess(user)); } catch (e) { emit(LoginFailure(e.toString())); } } } ``` Rules: - Trigger state changes via `bloc.add(Event())`, not custom public methods. - Keep event handler methods private (`_onEventName`). - Internal/repository events must be private and may use custom transformers. --- ## 6. Architecture Three layers — each must stay in its own boundary: ``` Presentation → Business Logic (Cubit/Bloc) → Data (Repository → DataProvider) ``` - **Data Layer**: Repositories wrap data providers. Providers perform raw CRUD (HTTP, DB). Repositories expose clean domain objects. - **Business Logic Layer**: Cubits/Blocs receive repository data and emit states. Inject repositories via constructor. - **Presentation Layer**: Renders UI based on state. Handles user input by calling cubit methods or adding bloc events. Rules: - Blocs must not access data providers directly — only via repositories. - No direct bloc-to-bloc communication. Use `BlocListener` in the UI to bridge blocs. - For shared data, inject the same repository into multiple blocs. - Initialize `BlocObserver` in `main.dart`. --- ## 7. Flutter Bloc Widgets | Widget | Use | |---|---| | `BlocProvider` | Provide a bloc to a subtree | | `MultiBlocProvider` | Provide multiple blocs without nesting | | `BlocBuilder` | Rebuild UI on state change | | `BlocListener` | Side effects only (navigation, dialogs, snackbars) | | `MultiBlocListener` | Listen to multiple blocs without nesting | | `BlocConsumer` | Rebuild UI + side effects together | | `BlocSelector` | Rebuild only when a selected slice of state changes | | `RepositoryProvider` | Provide a repository to the widget tree | | `MultiRepositoryProvider` | Provide multiple repositories without nesting | ```dart BlocProvider( create: (context) => LoginCubit(context.read<AuthRepository>()), child: LoginView(), ); BlocBuilder<LoginCubit, LoginState>( builder: (context, state) { return switch (state.status) { LoginStatus.loading => const CircularProgressIndicator(), LoginStatus.success => const HomeView(), LoginStatus.failure => Text(state.errorMessage ?? 'Error'), LoginStatus.initial => const LoginForm(), }; }, ); BlocListener<LoginCubit, LoginState>( listener: (context, state) { if (state.status == LoginStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.errorMessage ?? 'Login failed')), ); } }, child: LoginForm(), ); ``` Rules: - Use `context.read<T>()` in callbacks (not in `build`). - Use `context.watch<T>()` in `build` only when necessary; prefer `BlocBuilder`. - Never call `context.watch` or `context.select` at the root of `build` — scope with `Builder`. - Handle **all** possible states in the UI (initial, loading, success, failure). --- ## 8. Testing Use `bloc_test` package. Mock repositories with `mocktail`. ```dart import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; class MockAuthRepository extends Mock implements AuthRepository {} void main() { group('LoginCubit', () { late AuthRepository authRepository; late LoginCubit loginCubit; setUp(() { authRepository = MockAuthRepository(); loginCubit = LoginCubit(authRepository); }); tearDown(() => loginCubit.close()); test('initial state should be LoginState with status initial', () { expect(loginCubit.state, const LoginState()); }); blocTest<LoginCubit, LoginState>( 'should emit [loading, success] when login succeeds', build: () { when(() => authRepository.login(any(), any())) .thenAnswer((_) async => fakeUser); return loginCubit; }, act: (cubit) => cubit.login('email@test.com', 'password'), expect: () => [ const LoginState(status: LoginStatus.loading), LoginState(status: LoginStatus.success, user: fakeUser), ], ); blocTest<LoginCubit, LoginState>( 'should emit [loading, failure] when login throws', build: () { when(() => authRepository.login(any(), any())) .thenThrow(Exception('error')); return loginCubit; }, act: (cubit) => cubit.login('email@test.com', 'wrong'), expect: () => [ const LoginState(status: LoginStatus.loading), isA<LoginState>().having((s) => s.status, 'status', LoginStatus.failure), ], ); }); } ``` Rules: - Always call `tearDown(() => cubit.close())`. - Use `blocTest` for state emission assertions. - Use `group()` named after the class under test. - Name test cases with "should" to describe expected behavior. - Register fallback values for custom types: `registerFallbackValue(MyEvent())`. --- ## 9. Common Pitfalls | Pitfall | Fix | |---|---| | Emitting the same state instance twice | Always create a new state object; bloc ignores duplicate emissions via `==`. | | Calling `context.watch` inside callbacks | Use `context.read` in callbacks; `watch` is only valid inside `build`. | | Forgetting `Equatable` props | Add every field to `props`; missing fields cause silent state update bugs. | | Mutable state fields | Keep state `@immutable`; use `copyWith` or new sealed subclass instances. | | Business logic in widgets | Move all logic into the Cubit/Bloc; widgets only dispatch events or call methods. | ```dart // BAD — mutating state in-place state.items.add(newItem); emit(state); // GOOD — emit a new state with copied list emit(state.copyWith(items: [...state.items, newItem])); ``` --- ## References - [Bloc GitHub Repository](https://github.com/felangel/bloc)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.