{"id":"bloc","name":"bloc","summary":"CubitやBlocの作成、密閉クラスやステータス列挙で状態をモデリングする、BlocBuilder/BlocListener/BlocProviderの配線、ブロックテストの作成、CubitとBlocのどちらかを選ぶ際に使います。","body":"# Bloc Skill\n\nDesign, implement, and test state management using the [bloc](https://pub.dev/packages/bloc) and [flutter_bloc](https://pub.dev/packages/flutter_bloc) libraries.\n\n## When to Use\n\nUse this skill when:\n\n* Creating a new Cubit or Bloc for a feature.\n* Modeling state (choosing between sealed classes and a single state class with status enum).\n* Wiring `BlocBuilder`, `BlocListener`, `BlocConsumer`, or `BlocProvider` in the widget tree.\n* Writing unit tests for a Cubit or Bloc.\n* Deciding between Cubit and Bloc.\n* Refactoring existing state management to follow bloc conventions.\n\n---\n\n## 1. Cubit vs Bloc\n\n| Situation | Use |\n|---|---|\n| Simple state, no events needed | `Cubit` |\n| Complex flows, event traceability needed | `Bloc` |\n| Advanced event processing (debounce, throttle) | `Bloc` with event transformers |\n\n**Default to `Cubit`. Refactor to `Bloc` only when requirements grow.**\n\n---\n\n## 2. Naming Conventions\n\n### Events (Bloc only)\n- Named in **past tense**: `LoginButtonPressed`, `UserProfileLoaded`.\n- Format: `BlocSubject` + optional noun + verb.\n- Initial load event: `BlocSubjectStarted` (e.g., `AuthenticationStarted`).\n- Base event class: `BlocSubjectEvent`.\n\n### States\n- Named as **nouns** (states are snapshots in time).\n- Base state class: `BlocSubjectState`.\n- Sealed subclasses: `BlocSubject` + `Initial` | `InProgress` | `Success` | `Failure`.\n  - Example: `LoginInitial`, `LoginInProgress`, `LoginSuccess`, `LoginFailure`.\n- Single-class approach: `BlocSubjectState` + `BlocSubjectStatus` enum (`initial`, `loading`, `success`, `failure`).\n\n---\n\n## 3. Modeling State\n\n### When to use a sealed class with subclasses\n- States are **well-defined and mutually exclusive**.\n- Type-safe exhaustive `switch` is desired.\n- Subclass-specific properties exist.\n\n```dart\n@immutable\nsealed class LoginState extends Equatable {\n  const LoginState();\n}\n\nfinal class LoginInitial extends LoginState {\n  @override\n  List<Object?> get props => [];\n}\n\nfinal class LoginInProgress extends LoginState {\n  @override\n  List<Object?> get props => [];\n}\n\nfinal class LoginSuccess extends LoginState {\n  const LoginSuccess(this.user);\n  final User user;\n  @override\n  List<Object?> get props => [user];\n}\n\nfinal class LoginFailure extends LoginState {\n  const LoginFailure(this.message);\n  final String message;\n  @override\n  List<Object?> get props => [message];\n}\n```\n\nHandle all states exhaustively in the UI:\n```dart\nswitch (state) {\n  case LoginInitial():  ...\n  case LoginInProgress(): ...\n  case LoginSuccess(:final user): ...\n  case LoginFailure(:final message): ...\n}\n```\n\n### When to use a single class with a status enum\n- Many shared properties across states.\n- Simpler, more flexible; previous data must be retained after failure.\n\n```dart\nenum LoginStatus { initial, loading, success, failure }\n\n@immutable\nclass LoginState extends Equatable {\n  const LoginState({\n    this.status = LoginStatus.initial,\n    this.user,\n    this.errorMessage,\n  });\n\n  final LoginStatus status;\n  final User? user;\n  final String? errorMessage;\n\n  LoginState copyWith({\n    LoginStatus? status,\n    User? user,\n    String? errorMessage,\n  }) {\n    return LoginState(\n      status: status ?? this.status,\n      user: user ?? this.user,\n      errorMessage: errorMessage ?? this.errorMessage,\n    );\n  }\n\n  @override\n  List<Object?> get props => [status, user, errorMessage];\n}\n```\n\n### State rules (both approaches)\n- Extend `Equatable` and pass all relevant fields to `props`.\n- Copy `List`/`Map` properties with `List.of`/`Map.of` inside `props`.\n- Annotate with `@immutable`.\n- Always emit a **new instance**; never reuse the same state object.\n- Duplicate states are ignored by bloc — ensure meaningful state changes.\n\n---\n\n## 4. Cubit Implementation\n\n```dart\nclass LoginCubit extends Cubit<LoginState> {\n  LoginCubit(this._authRepository) : super(const LoginState());\n\n  final AuthRepository _authRepository;\n\n  Future<void> login(String email, String password) async {\n    emit(state.copyWith(status: LoginStatus.loading));\n    try {\n      final user = await _authRepository.login(email, password);\n      emit(state.copyWith(status: LoginStatus.success, user: user));\n    } catch (e) {\n      emit(state.copyWith(status: LoginStatus.failure, errorMessage: e.toString()));\n    }\n  }\n}\n```\n\nRules:\n- Only call `emit` inside the Cubit/Bloc.\n- Public methods return `void` or `Future<void>` only.\n- Keep business logic out of UI.\n- When overriding `storage` in a `HydratedCubit`, pass it as a named parameter: `super(initialState, storage: storage)`.\n\n---\n\n## 5. Bloc Implementation\n\n```dart\nsealed class LoginEvent {}\nfinal class LoginSubmitted extends LoginEvent {\n  LoginSubmitted({required this.email, required this.password});\n  final String email;\n  final String password;\n}\n\nclass LoginBloc extends Bloc<LoginEvent, LoginState> {\n  LoginBloc(this._authRepository) : super(LoginInitial()) {\n    on<LoginSubmitted>(_onLoginSubmitted);\n  }\n\n  final AuthRepository _authRepository;\n\n  Future<void> _onLoginSubmitted(\n    LoginSubmitted event,\n    Emitter<LoginState> emit,\n  ) async {\n    emit(LoginInProgress());\n    try {\n      final user = await _authRepository.login(event.email, event.password);\n      emit(LoginSuccess(user));\n    } catch (e) {\n      emit(LoginFailure(e.toString()));\n    }\n  }\n}\n```\n\nRules:\n- Trigger state changes via `bloc.add(Event())`, not custom public methods.\n- Keep event handler methods private (`_onEventName`).\n- Internal/repository events must be private and may use custom transformers.\n\n---\n\n## 6. Architecture\n\nThree layers — each must stay in its own boundary:\n\n```\nPresentation  →  Business Logic (Cubit/Bloc)  →  Data (Repository → DataProvider)\n```\n\n- **Data Layer**: Repositories wrap data providers. Providers perform raw CRUD (HTTP, DB). Repositories expose clean domain objects.\n- **Business Logic Layer**: Cubits/Blocs receive repository data and emit states. Inject repositories via constructor.\n- **Presentation Layer**: Renders UI based on state. Handles user input by calling cubit methods or adding bloc events.\n\nRules:\n- Blocs must not access data providers directly — only via repositories.\n- No direct bloc-to-bloc communication. Use `BlocListener` in the UI to bridge blocs.\n- For shared data, inject the same repository into multiple blocs.\n- Initialize `BlocObserver` in `main.dart`.\n\n---\n\n## 7. Flutter Bloc Widgets\n\n| Widget | Use |\n|---|---|\n| `BlocProvider` | Provide a bloc to a subtree |\n| `MultiBlocProvider` | Provide multiple blocs without nesting |\n| `BlocBuilder` | Rebuild UI on state change |\n| `BlocListener` | Side effects only (navigation, dialogs, snackbars) |\n| `MultiBlocListener` | Listen to multiple blocs without nesting |\n| `BlocConsumer` | Rebuild UI + side effects together |\n| `BlocSelector` | Rebuild only when a selected slice of state changes |\n| `RepositoryProvider` | Provide a repository to the widget tree |\n| `MultiRepositoryProvider` | Provide multiple repositories without nesting |\n\n```dart\nBlocProvider(\n  create: (context) => LoginCubit(context.read<AuthRepository>()),\n  child: LoginView(),\n);\n\nBlocBuilder<LoginCubit, LoginState>(\n  builder: (context, state) {\n    return switch (state.status) {\n      LoginStatus.loading => const CircularProgressIndicator(),\n      LoginStatus.success => const HomeView(),\n      LoginStatus.failure => Text(state.errorMessage ?? 'Error'),\n      LoginStatus.initial => const LoginForm(),\n    };\n  },\n);\n\nBlocListener<LoginCubit, LoginState>(\n  listener: (context, state) {\n    if (state.status == LoginStatus.failure) {\n      ScaffoldMessenger.of(context).showSnackBar(\n        SnackBar(content: Text(state.errorMessage ?? 'Login failed')),\n      );\n    }\n  },\n  child: LoginForm(),\n);\n```\n\nRules:\n- Use `context.read<T>()` in callbacks (not in `build`).\n- Use `context.watch<T>()` in `build` only when necessary; prefer `BlocBuilder`.\n- Never call `context.watch` or `context.select` at the root of `build` — scope with `Builder`.\n- Handle **all** possible states in the UI (initial, loading, success, failure).\n\n---\n\n## 8. Testing\n\nUse `bloc_test` package. Mock repositories with `mocktail`.\n\n```dart\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass MockAuthRepository extends Mock implements AuthRepository {}\n\nvoid main() {\n  group('LoginCubit', () {\n    late AuthRepository authRepository;\n    late LoginCubit loginCubit;\n\n    setUp(() {\n      authRepository = MockAuthRepository();\n      loginCubit = LoginCubit(authRepository);\n    });\n\n    tearDown(() => loginCubit.close());\n\n    test('initial state should be LoginState with status initial', () {\n      expect(loginCubit.state, const LoginState());\n    });\n\n    blocTest<LoginCubit, LoginState>(\n      'should emit [loading, success] when login succeeds',\n      build: () {\n        when(() => authRepository.login(any(), any()))\n            .thenAnswer((_) async => fakeUser);\n        return loginCubit;\n      },\n      act: (cubit) => cubit.login('email@test.com', 'password'),\n      expect: () => [\n        const LoginState(status: LoginStatus.loading),\n        LoginState(status: LoginStatus.success, user: fakeUser),\n      ],\n    );\n\n    blocTest<LoginCubit, LoginState>(\n      'should emit [loading, failure] when login throws',\n      build: () {\n        when(() => authRepository.login(any(), any()))\n            .thenThrow(Exception('error'));\n        return loginCubit;\n      },\n      act: (cubit) => cubit.login('email@test.com', 'wrong'),\n      expect: () => [\n        const LoginState(status: LoginStatus.loading),\n        isA<LoginState>().having((s) => s.status, 'status', LoginStatus.failure),\n      ],\n    );\n  });\n}\n```\n\nRules:\n- Always call `tearDown(() => cubit.close())`.\n- Use `blocTest` for state emission assertions.\n- Use `group()` named after the class under test.\n- Name test cases with \"should\" to describe expected behavior.\n- Register fallback values for custom types: `registerFallbackValue(MyEvent())`.\n\n---\n\n## 9. Common Pitfalls\n\n| Pitfall | Fix |\n|---|---|\n| Emitting the same state instance twice | Always create a new state object; bloc ignores duplicate emissions via `==`. |\n| Calling `context.watch` inside callbacks | Use `context.read` in callbacks; `watch` is only valid inside `build`. |\n| Forgetting `Equatable` props | Add every field to `props`; missing fields cause silent state update bugs. |\n| Mutable state fields | Keep state `@immutable`; use `copyWith` or new sealed subclass instances. |\n| Business logic in widgets | Move all logic into the Cubit/Bloc; widgets only dispatch events or call methods. |\n\n```dart\n// BAD — mutating state in-place\nstate.items.add(newItem);\nemit(state);\n\n// GOOD — emit a new state with copied list\nemit(state.copyWith(items: [...state.items, newItem]));\n```\n\n---\n\n## References\n\n- [Bloc GitHub Repository](https://github.com/felangel/bloc)","author":"@evanca","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/evanca/flutter-ai-rules/tree/main/skills/bloc","license":"MIT","category":"writing","lang":"en","tokens":2546,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["pub.dev"]}}