Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ npx skills update
| [flutter-setup-declarative-routing](skills/flutter-setup-declarative-routing/SKILL.md) | Configure `MaterialApp.router` using a package like `go_router` for advanced URL-based navigation. Use when developing web applications or mobile apps that require specific deep linking and browser history support. | Set up GoRouter with paths for home, details, and settings |
| [flutter-setup-localization](skills/flutter-setup-localization/SKILL.md) | Add `flutter_localizations` and `intl` dependencies, enable "generate true" in `pubspec.yaml`, and create an `l10n.yaml` configuration file. Use when initializing localization support for a new Flutter project. | Setup localization and add English and Spanish translations |
| [flutter-use-http-package](skills/flutter-use-http-package/SKILL.md) | Use the `http` package to execute GET, POST, PUT, or DELETE requests. Use when you need to fetch from or send data to a REST API. | Use the http package to fetch the list of products from the API |
| [flutter-bloc-cubit-feature](skills/flutter-bloc-cubit-feature/SKILL.md) | Scaffold or update Flutter features using flutter_bloc and cubit/bloc state management with a feature-first layered structure. Use when creating a new feature, deciding between Cubit and Bloc, adding Freezed states, wiring repositories, or adding tests for Bloc/Cubit logic. | Create a new feature with flutter_bloc cubit and a layered folder structure |
## Contributing

We aren't accepting pull requests at this time, but we would love to hear your feedback!
Expand Down
22 changes: 22 additions & 0 deletions resources/flutter_skills.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,28 @@
- https://docs.flutter.dev/resources/architectural-overview
- https://docs.flutter.dev/resources/faq
- https://docs.flutter.dev/ui/layout/tutorial
- name: flutter-bloc-cubit-feature
description: Scaffold or update Flutter features using flutter_bloc and
cubit/bloc state management with a feature-first layered structure. Use when
creating a new feature, deciding between Cubit and Bloc, adding Freezed
states, wiring repositories, or adding tests for Bloc/Cubit logic.
examplePrompt: "Create a new feature with flutter_bloc cubit and a layered folder structure"
instructions: |
1. **Inspect the existing project.** Identify feature folder conventions, DI style, routing package, error/result wrappers, and test framework before generating code.
2. **Choose Cubit or Bloc.** Use Cubit for linear command-style flows. Use Bloc for event-heavy, debounced, subscription, or stream-heavy flows.
3. **Create the feature folders.** Place models and repositories in `data`, Bloc/Cubit state logic in `logic`, and screens/widgets in `ui`.
4. **Implement state with Freezed when available.** Include explicit `initial`, `loading`, `loaded`, and `error` states.
5. **Wire repository calls.** Inject repositories through constructors. Map result wrappers into success, error, and transition states.
6. **Keep rendering and side effects separate.** Use `BlocBuilder` for rendering and `BlocListener` or `BlocConsumer` for navigation, snackbars, and one-shot effects.
7. **Register dependencies.** Register API services, repositories as lazy singletons, and feature Cubits/Blocs as factories unless they own shared runtime state.
8. **Add routing scope.** Use route-level `BlocProvider` or `MultiBlocProvider`. Use `BlocProvider.value(...)` only to continue an existing flow.
9. **Add tests.** Test emitted state sequences, repository interactions, pagination behavior, and ownership-sensitive decisions.
10. **Run generators and tests.** Run `dart run build_runner build --delete-conflicting-outputs` after changing Freezed/JSON annotations, then run `flutter test`.
resources:
- https://pub.dev/packages/flutter_bloc
- https://bloclibrary.dev
- https://pub.dev/packages/freezed
- https://pub.dev/packages/mocktail
- name: flutter-build-responsive-layout
description: Use `LayoutBuilder`, `MediaQuery`, or `Expanded/Flexible` to
create a layout that adapts to different screen sizes. Use when you need the
Expand Down
267 changes: 267 additions & 0 deletions skills/flutter-bloc-cubit-feature/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
---
name: flutter-bloc-cubit-feature
description: Scaffold or update Flutter features using flutter_bloc and cubit/bloc state management with a feature-first layered structure. Use when creating a new feature, deciding between Cubit and Bloc, adding Freezed states, wiring repositories, adding route-level BlocProvider/MultiBlocProvider scope, registering dependencies, implementing pagination, handling UI side effects, or adding tests for Bloc/Cubit logic.
metadata:
model: models/gemini-3.1-pro-preview
---

# Implementing Flutter Bloc/Cubit Features

## Contents
- [Cubit vs Bloc Decision](#cubit-vs-bloc-decision)
- [Feature Structure](#feature-structure)
- [Workflow: Implementing a Feature](#workflow-implementing-a-feature)
- [State, Side Effects, and Errors](#state-side-effects-and-errors)
- [Dependency Injection and Routing](#dependency-injection-and-routing)
- [Pagination](#pagination)
- [Testing](#testing)
- [Examples](#examples)

## Cubit vs Bloc Decision

Choose the lightest state-management primitive that fits the feature.

Use **Cubit** when the flow is mostly command-style methods such as `load`, `refresh`, `submit`, `select`, `update`, or `retry`, and no complex event stream coordination is needed.

Use **Bloc** when the feature has multiple event types, event transformers, debounced input, subscriptions, stream coordination, pagination filters, or richer event/state transitions.

When unclear, start with Cubit. Upgrade to Bloc only when events and transformers make the behavior easier to reason about.

## Feature Structure

Prefer feature-first organization with `data`, `logic`, and `ui` layers. Put Bloc/Cubit files under the feature's `logic` folder.

```text
lib/src/features/<group>/<feature>/
data/
models/
repos/
datasources/ # optional
logic/
cubit/ # default for command-style flows
bloc/ # event-heavy or stream-heavy flows
dtos/ # optional
view_models/ # optional
ui/
screens/
widgets/
dialogs/ # optional
```

If the target project does not use `lib/src/features/<group>/<feature>`, adapt the folder names to the existing project while preserving the `data` / `logic` / `ui` separation.

## Workflow: Implementing a Feature

Copy this checklist when creating or refactoring a feature.

### Task Progress

- [ ] **Step 1: Inspect the existing project.** Identify feature folder conventions, DI style, routing package, error/result wrappers, and test framework before generating code.
- [ ] **Step 2: Choose Cubit or Bloc.** Use Cubit for linear commands. Use Bloc for event-heavy, stream-heavy, debounced, subscription, or transformer-based flows.
- [ ] **Step 3: Create or update the feature folders.** Place models and repositories in `data`, Bloc/Cubit state logic in `logic`, and screens/widgets/dialogs in `ui`.
- [ ] **Step 4: Implement state with Freezed when available.** Include explicit `initial`, `loading`, `loaded`, `error`, and side-effect states when the project uses Freezed unions.
- [ ] **Step 5: Wire repository calls.** Inject repositories through constructors. Map result wrappers such as `ApiResult<T>.when(...)` into success, error, and transition states.
- [ ] **Step 6: Keep rendering and side effects separate.** Use `BlocBuilder` for rendering and `BlocListener` or `BlocConsumer` for navigation, snackbars, dialogs, overlays, and one-shot effects.
- [ ] **Step 7: Register dependencies.** Register API services if needed, repositories as lazy singletons, and feature Cubits/Blocs as factories unless they intentionally own app-wide runtime state.
- [ ] **Step 8: Add routing scope.** Prefer route-level `BlocProvider` or `MultiBlocProvider`. Use `BlocProvider.value(...)` only to continue an existing in-memory flow across screens.
- [ ] **Step 9: Add tests.** Test emitted state sequences, repository interactions, pagination refresh/failure behavior, and ownership-sensitive decisions.
- [ ] **Step 10: Run generators and tests.** Run `dart run build_runner build --delete-conflicting-outputs` after changing Freezed/JSON/Retrofit annotations, then run `flutter test`.

## State, Side Effects, and Errors

Use explicit state names. Common states are `initial`, `loading`, `loaded`, and `error`. Model one-shot UI effects as state variants when that is the project convention, using names such as `navigateToDetails`, `showSuccess`, `showError`, `hideDialog`, or `itemUpdated`.

Repositories should catch errors, normalize them through the project's error handler, and return the project's result type. Logic should map failures into user-facing error states or listener-handled side effects.

Avoid returning `null` for failures. Emit an explicit error state instead.

## Dependency Injection and Routing

Follow the project's existing DI entrypoint. If the project uses `get_it`, prefer this pattern:

- register API clients/services as lazy singletons
- register repositories as lazy singletons
- register feature Cubits/Blocs as factories
- register app-wide runtime owners as singletons only when they intentionally own shared runtime state

Route-level provider scope is the default for screen-local state. Do not pass shared runtime/session data through route extras when a shared owner already exists. Use typed route extras only for route-local flow state or reusing an active Cubit/Bloc across screens.

## Shared Runtime Ownership

Before adding a new field, helper, or singleton, decide whether the state is shared runtime or feature-local.

Put account/session runtime in a shared owner such as `SessionContextCubit`: current user summary, plan/subscription summary, onboarding flags, account-completion flags, and unread notification count.

Put workspace/store runtime in a shared owner such as `AppModeCubit`: active mode, active workspace/shop/store identifiers, accessible workspaces, and active summaries.

Put derived UX capability checks in a pure entitlement/capability layer such as `SessionEntitlements`. Do not duplicate raw plan-based gating inside feature widgets when an entitlement already models the decision.

Keep feature-owned state in the feature Bloc/Cubit: lists, filters, forms, wizard state, selected tabs, draft edits, pagination controllers, and screen-specific loading/error state.

## Pagination

When using `infinite_scroll_pagination`, own the `PagingController` inside the Cubit or Bloc logic, not in the widget.

- initialize the controller with the project package version's expected callbacks, such as `getNextPageKey` and `fetchPage` when available
- call `refresh()` for reloads
- expose the controller to the UI through the Cubit/Bloc when that is the established convention
- throw or map a `PagingException` on fetch failures when the package supports it
- dispose the controller in `close()`

## Testing

Place unit tests under the project's `test/` tree, mirroring the feature path when possible. Use the project's mocking package, commonly `mocktail`.

Test the important behavior rather than every implementation detail:

- Cubit/Bloc initial state
- emitted state sequence on success
- emitted state sequence on failure
- repository method calls and arguments
- pagination refresh and next-page behavior
- side-effect states such as navigation or success messages
- shared-runtime ownership decisions when the feature reads session, mode, or entitlement state

## Examples

### Minimal Cubit with Freezed State

```dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

import '../../data/repos/feature_repository.dart';
import '../../../../core/networking/error/api_error_model.dart';

part 'feature_cubit.freezed.dart';
part 'feature_state.dart';

class FeatureCubit extends Cubit<FeatureState> {
FeatureCubit(this._repository) : super(const FeatureState.initial());

final FeatureRepository _repository;

Future<void> loadData() async {
emit(const FeatureState.loading());

final result = await _repository.getData();
result.when(
success: (data) => emit(FeatureState.loaded(data)),
failure: (error) => emit(FeatureState.error(error)),
);
}
}
```

```dart
part of 'feature_cubit.dart';

@freezed
class FeatureState with _$FeatureState {
const factory FeatureState.initial() = _Initial;
const factory FeatureState.loading() = _Loading;
const factory FeatureState.loaded(FeatureViewData data) = _Loaded;
const factory FeatureState.error(ApiErrorModel error) = _Error;
const factory FeatureState.showSuccess(String message) = _ShowSuccess;
}
```

### Route-Level Provider Scope

```dart
GoRoute(
path: '/feature',
builder: (context, state) {
return BlocProvider(
create: (_) => getIt<FeatureCubit>()..loadData(),
child: const FeatureScreen(),
);
},
)
```

### UI Rendering and Listener Separation

```dart
BlocConsumer<FeatureCubit, FeatureState>(
listener: (context, state) {
state.whenOrNull(
showSuccess: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
);
},
builder: (context, state) {
return state.maybeWhen(
loading: () => const Center(child: CircularProgressIndicator()),
loaded: (data) => FeatureContent(data: data),
error: (error) => ErrorView(message: error.message),
orElse: () => const SizedBox.shrink(),
);
},
)
Comment on lines +186 to +204

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In this example, emitting a side-effect state like showSuccess will cause the BlocConsumer's builder to rebuild. Since showSuccess is not handled in maybeWhen, it falls back to orElse and returns SizedBox.shrink(), causing the UI to go blank when the snackbar is shown.

To prevent this, use buildWhen and listenWhen to separate side-effect states from UI-rendering states, or ensure side-effect states carry the current screen data.

BlocConsumer<FeatureCubit, FeatureState>(
  listenWhen: (previous, current) => current.maybeMap(
    showSuccess: (_) => true,
    orElse: () => false,
  ),
  listener: (context, state) {
    state.whenOrNull(
      showSuccess: (message) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(message)),
        );
      },
    );
  },
  buildWhen: (previous, current) => current.maybeMap(
    showSuccess: (_) => false,
    orElse: () => true,
  ),
  builder: (context, state) {
    return state.maybeWhen(
      loading: () => const Center(child: CircularProgressIndicator()),
      loaded: (data) => FeatureContent(data: data),
      error: (error) => ErrorView(message: error.message),
      orElse: () => const SizedBox.shrink(),
    );
  },
)

```

### Bloc When Events Are Richer

```dart
sealed class OrdersEvent {
const OrdersEvent();
}

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

final class OrdersFilterChanged extends OrdersEvent {
const OrdersFilterChanged(this.filter);
final OrdersFilter filter;
}

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

class OrdersBloc extends Bloc<OrdersEvent, OrdersState> {
OrdersBloc(this._repository) : super(const OrdersState.initial()) {
on<OrdersStarted>(_onStarted);
on<OrdersFilterChanged>(_onFilterChanged);
on<OrdersRefreshed>(_onRefreshed);
}

final OrdersRepository _repository;

Future<void> _onStarted(
OrdersStarted event,
Emitter<OrdersState> emit,
) async {
emit(const OrdersState.loading());
final result = await _repository.getOrders();
result.when(
success: (orders) => emit(OrdersState.loaded(orders)),
failure: (error) => emit(OrdersState.error(error)),
);
}

Future<void> _onFilterChanged(
OrdersFilterChanged event,
Emitter<OrdersState> emit,
) async {
emit(OrdersState.filterChanged(event.filter));
add(const OrdersRefreshed());
}

Future<void> _onRefreshed(
OrdersRefreshed event,
Emitter<OrdersState> emit,
) async {
final result = await _repository.getOrders();
result.when(
success: (orders) => emit(OrdersState.loaded(orders)),
failure: (error) => emit(OrdersState.error(error)),
);
}
}
Comment on lines +218 to +266

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the OrdersBloc example, when OrdersFilterChanged is received, it emits filterChanged and adds OrdersRefreshed. However, _repository.getOrders() is called without any filter argument, meaning the changed filter is completely ignored during the data fetch.

To fix this, update OrdersRefreshed to accept the filter and pass it to the repository call.

final class OrdersFilterChanged extends OrdersEvent {
  const OrdersFilterChanged(this.filter);
  final OrdersFilter filter;
}

final class OrdersRefreshed extends OrdersEvent {
  const OrdersRefreshed(this.filter);
  final OrdersFilter filter;
}

class OrdersBloc extends Bloc<OrdersEvent, OrdersState> {
  OrdersBloc(this._repository) : super(const OrdersState.initial()) {
    on<OrdersStarted>(_onStarted);
    on<OrdersFilterChanged>(_onFilterChanged);
    on<OrdersRefreshed>(_onRefreshed);
  }

  final OrdersRepository _repository;

  Future<void> _onStarted(
    OrdersStarted event,
    Emitter<OrdersState> emit,
  ) async {
    emit(const OrdersState.loading());
    final result = await _repository.getOrders();
    result.when(
      success: (orders) => emit(OrdersState.loaded(orders)),
      failure: (error) => emit(OrdersState.error(error)),
    );
  }

  Future<void> _onFilterChanged(
    OrdersFilterChanged event,
    Emitter<OrdersState> emit,
  ) async {
    emit(OrdersState.filterChanged(event.filter));
    add(OrdersRefreshed(event.filter));
  }

  Future<void> _onRefreshed(
    OrdersRefreshed event,
    Emitter<OrdersState> emit,
  ) async {
    final result = await _repository.getOrders(filter: event.filter);
    result.when(
      success: (orders) => emit(OrdersState.loaded(orders)),
      failure: (error) => emit(OrdersState.error(error)),
    );
  }
}

```