Skip to content

Add Flutter Bloc/Cubit feature skill#163

Open
AbdelghaniDjedidi2001 wants to merge 3 commits into
flutter:mainfrom
AbdelghaniDjedidi2001:add-flutter-bloc-cubit-feature-skill
Open

Add Flutter Bloc/Cubit feature skill#163
AbdelghaniDjedidi2001 wants to merge 3 commits into
flutter:mainfrom
AbdelghaniDjedidi2001:add-flutter-bloc-cubit-feature-skill

Conversation

@AbdelghaniDjedidi2001

Copy link
Copy Markdown

Summary

Adds a new flutter-bloc-cubit-feature skill for scaffolding Flutter features using Bloc/Cubit with layered architecture.

The skill covers:

  • choosing Cubit for command-style flows
  • choosing Bloc for event-heavy or stream-heavy flows
  • feature-first data/, logic/, ui/ structure
  • Freezed state modeling
  • repository integration
  • route-level provider wiring
  • DI registration guidance
  • pagination and side-effect handling
  • targeted tests

Closes #162

Validation

  • Checked skill instructions manually
  • Verified the skill follows the compact format used by existing Flutter skills
  • No runtime code changes

@google-cla

google-cla Bot commented Jun 14, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces a new skill, flutter-bloc-cubit-feature, which provides guidelines and examples for scaffolding and updating Flutter features using flutter_bloc and Cubit/Bloc state management. The feedback highlights two critical issues in the provided code examples: first, the BlocConsumer example causes the UI to go blank when a side-effect state is emitted because it lacks buildWhen and listenWhen filtering; second, the OrdersBloc example ignores the updated filter during data fetching because the filter is not passed to the repository call.

Comment on lines +186 to +204
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(),
);
},
)

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(),
    );
  },
)

Comment on lines +218 to +266
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)),
);
}
}

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)),
    );
  }
}

@AbdelghaniDjedidi2001 AbdelghaniDjedidi2001 marked this pull request as draft June 14, 2026 12:24
@AbdelghaniDjedidi2001 AbdelghaniDjedidi2001 marked this pull request as ready for review June 14, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Skill idea] flutter-bloc-cubit-feature — scaffold Bloc/Cubit features with layered architecture

1 participant