bloc

$npx mdskill add evanca/flutter-ai-rules/bloc

Design Flutter state management with Bloc or Cubit libraries.

  • Model state and wire widgets for new features.
  • Integrates with Flutter, Bloc, Cubit, and flutter_bloc.
  • Decides between Cubit and Bloc based on complexity.
  • Generates sealed classes or event transformers for logic.
SKILL.md
.github/skills/blocView on GitHub ↗
---
name: bloc
description: Implements Flutter state management using the bloc library (Bloc and Cubit). Use when creating new features, screens, or state management logic with bloc/cubit, modeling state, wiring Flutter widgets to blocs, or writing bloc/cubit unit tests.
---

# Bloc Skill

This skill defines how to 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 vs a single state class).
* Wiring `BlocBuilder`, `BlocListener`, `BlocConsumer`, or `BlocProvider` in the widget tree.
* Writing unit tests for a Cubit or Bloc.
* Deciding between Cubit and Bloc.

---

## 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.

---

## 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())`.

---

## References

- [Bloc GitHub Repository](https://github.com/felangel/bloc)
More from evanca/flutter-ai-rules