Add Flutter Bloc/Cubit feature skill#163
Conversation
|
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. |
There was a problem hiding this comment.
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.
| 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(), | ||
| ); | ||
| }, | ||
| ) |
There was a problem hiding this comment.
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(),
);
},
)| 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)), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
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)),
);
}
}
Summary
Adds a new
flutter-bloc-cubit-featureskill for scaffolding Flutter features using Bloc/Cubit with layered architecture.The skill covers:
data/,logic/,ui/structureCloses #162
Validation