--- 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 get props => []; } final class LoginInProgress extends LoginState { @override List get props => []; } final class LoginSuccess extends LoginState { const LoginSuccess(this.user); final User user; @override List get props => [user]; } final class LoginFailure extends LoginState { const LoginFailure(this.message); final String message; @override List 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 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 { LoginCubit(this._authRepository) : super(const LoginState()); final AuthRepository _authRepository; Future 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` 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 { LoginBloc(this._authRepository) : super(LoginInitial()) { on(_onLoginSubmitted); } final AuthRepository _authRepository; Future _onLoginSubmitted( LoginSubmitted event, Emitter 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()), child: LoginView(), ); BlocBuilder( 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( 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()` in callbacks (not in `build`). - Use `context.watch()` 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( '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( '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().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)