home / skills / yelmuratoff / agent_sync / bloc

bloc skill

/.ai/src/skills/bloc

This skill helps you implement and maintain robust Flutter state with BLoC, Cubit, or Provider patterns, improving scalability and testability.

npx playbooks add skill yelmuratoff/agent_sync --skill bloc

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.5 KB
---
name: bloc
description: When implementing feature state using BLoC (default), Cubit (small UI state), or Provider (lightweight UI controllers).
---

# State Management (BLoC Default)

## When to use

- BLoC: default for feature flows, async orchestration, and business-facing UI state.
- Cubit: small UI-only state (toggles, selected tab) where events/transformers are unnecessary.
- Provider: a lightweight UI controller (e.g., filters) when it stays UI-scoped and does not contain business logic.

## Steps

### 1) Choose the right tool

Quick rule:

- If it coordinates async work and talks to repositories: BLoC.
- If it only holds ephemeral UI state: Cubit.
- If it’s a tiny widget-scoped controller and BLoC would be noise: Provider.

State-shape rule:

- Prefer `sealed` states when each state has distinct payloads (`Loading/Loaded/Error`).
- For progressive forms where previously entered values must survive status changes, use one immutable state + enum status + `copyWith`.

### 2) Define events (sealed, manual)

```dart
part of 'orders_bloc.dart';

sealed class OrdersEvent {
  const OrdersEvent();
}

final class OrdersStartedEvent extends OrdersEvent {
  const OrdersStartedEvent();
}

final class OrdersRefreshEvent extends OrdersEvent {
  const OrdersRefreshEvent();
}
```

### 3) Define states (sealed, minimal, Equatable only when needed)

```dart
part of 'orders_bloc.dart';

sealed class OrdersState {
  const OrdersState();
}

final class OrdersInitialState extends OrdersState {
  const OrdersInitialState();
}

final class OrdersLoadingState extends OrdersState {
  const OrdersLoadingState();
}

final class OrdersLoadedState extends OrdersState with EquatableMixin {
  const OrdersLoadedState({required this.orders});

  final List<OrderDto> orders;

  @override
  List<Object?> get props => [orders];
}

final class OrdersErrorState extends OrdersState with EquatableMixin {
  const OrdersErrorState({
    required this.message,
    this.error,
    this.stackTrace,
  });

  final String? message;
  final Object? error;
  final StackTrace? stackTrace;

  @override
  List<Object?> get props => [message, error, stackTrace];
}
```

### 4) Implement the BLoC with explicit concurrency

Pick the transformer intentionally:

- `droppable()` for “tap spam should not queue”
- `restartable()` for “latest wins” (search, refresh)
- `sequential()` for strict ordering

Example with `restartable()`:

```dart
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';

part 'orders_event.dart';
part 'orders_state.dart';

final class OrdersBloc extends Bloc<OrdersEvent, OrdersState> {
  OrdersBloc({required this.repository}) : super(const OrdersInitialState()) {
    on<OrdersEvent>(
      (event, emit) => switch (event) {
        OrdersStartedEvent() => _load(emit),
        OrdersRefreshEvent() => _load(emit),
      },
      transformer: restartable(),
    );
  }

  final IOrdersRepository repository;

  Future<void> _load(Emitter<OrdersState> emit) async {
    emit(const OrdersLoadingState());
    try {
      final orders = await repository.getOrders();
      emit(OrdersLoadedState(orders: orders));
    // Known exceptions: catch specifically, emit error state, do NOT call onError.
    // Unexpected exceptions: fall through to outer catch, call onError(e, st).
    } catch (e, st) {
      handleException(
        exception: e,
        stackTrace: st,
        onError: (message, _, _, _) => emit(
          OrdersErrorState(message: message, error: e, stackTrace: st),
        ),
      );
    }
  }
}
```

### 5) Keep business logic out of widgets

BLoC orchestrates UI state; business rules live in repositories/services (or in small injected helpers).

If the BLoC grows because of data formatting:

- move formatting to DTO extensions
- move procedural logic to an injected service

### 6) Test BLoCs at the boundary

Use `bloc_test` and mock repositories. Cover:

- success path
- expected failures (network/timeout/cache)
- concurrency behavior (e.g., restartable cancels previous)
- order-sensitive event tests (insert `await Future<void>.delayed(Duration.zero)` between `add(...)` calls when needed)

Example:

```dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class _OrdersRepositoryMock extends Mock implements IOrdersRepository {}

void main() {
  late IOrdersRepository repo;

  setUp(() => repo = _OrdersRepositoryMock());

  blocTest<OrdersBloc, OrdersState>(
    'emits [Loading, Loaded] on success',
    build: () {
      when(() => repo.getOrders()).thenAnswer((_) async => const []);
      return OrdersBloc(repository: repo);
    },
    act: (bloc) => bloc.add(const OrdersStartedEvent()),
    expect: () => [
      const OrdersLoadingState(),
      const OrdersLoadedState(orders: []),
    ],
  );
}
```

### 7) Verify anti-patterns are avoided

Before finishing, check:

- [ ] No `ShowDialog`, `Navigate*`, or other UI-command states emitted from the BLoC. Side effects belong in `BlocListener` in the widget layer.
- [ ] No direct BLoC dependencies in the constructor. BLoC-to-BLoC synchronization must go through the widget layer.
- [ ] Error handling uses two tiers: known exceptions → emit error state only; unexpected exceptions → emit error state AND call `onError(e, st)`.
- [ ] BLoC is not managing simple UI-only state. If it is a toggle or a filter with no async work, downgrade to Cubit or `ValueNotifier`.
- [ ] If the success listener needs to distinguish *why* success was reached, `lastEvent` is stored in the state.

Overview

This skill guides implementing feature state using BLoC (default), Cubit (small UI state), or Provider (lightweight UI controllers). It focuses on choosing the right tool, defining sealed events and states, applying explicit concurrency, keeping business logic out of widgets, and testing at the boundary.

How this skill works

The skill inspects common state-shape patterns and prescribes sealed states versus single immutable state with enum status. It enforces explicit event handling, recommends transformers (droppable, restartable, sequential) for concurrency, and shows how to catch known vs unexpected exceptions. It also outlines test cases and anti-pattern checks to keep UI and business logic properly separated.

When to use it

  • Use BLoC for feature flows that coordinate async work and interact with repositories.
  • Use Cubit for small, ephemeral UI-only state like toggles or selected tabs.
  • Use Provider for tiny widget-scoped controllers where introducing BLoC would be noise.
  • Prefer sealed states when each state has distinct payloads (Loading/Loaded/Error).
  • Use immutable state + enum status for progressive forms that must preserve partially-entered values.

Best practices

  • Define events and states as sealed classes and keep states minimal; add Equatable only when needed for value comparisons.
  • Pick a concurrency transformer intentionally: droppable for tap spam, restartable for “latest wins,” sequential for strict ordering.
  • Keep business logic out of widgets; push formatting and procedural logic to DTO extensions or injected services.
  • Handle exceptions in two tiers: known exceptions → emit error state; unexpected exceptions → emit error state and call onError(e, st).
  • Test BLoCs with bloc_test and mocked repositories; cover success, known failures, concurrency, and ordering behaviors.

Example use cases

  • Orders list feature: BLoC coordinates repository fetch, emits Loading/Loaded/Error and uses restartable() for refresh/search.
  • Settings toggle: use Cubit to manage single boolean values without events or transformers.
  • Small filter controller scoped to a widget: use Provider when state and logic are tiny and UI-scoped.
  • Form with progressive steps: use one immutable state + status enum and copyWith so previously entered values persist across status changes.
  • Search bar: implement as BLoC/Cubit with restartable() transformer so the latest query cancels previous work.

FAQ

When should I prefer Cubit over BLoC?

Choose Cubit for ephemeral, UI-only state where events and concurrency transformers add no value—toggles, simple selections, or single-field state.

How do I decide between sealed states vs single-state-with-status?

Use sealed states when states carry distinct payloads. Use a single immutable state with a status enum when you must preserve previously entered values across status changes.