fl-bloc-pattern
Implements BLoC state management using CoreBlocBase, an abstract State hierarchy, and a freezed _StateData
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Implements BLoC state management using CoreBlocBase, an abstract State hierarchy, and a freezed _StateData
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Behavioral guidelines for Flutter base tasks: clarify ambiguity, keep changes simple and surgical, and define verifiable success criteria before coding.
Scaffolds a new feature module under apps/main/lib/presentation/modules using the bundled module generator
Reviews UI-layer changes — screens, blocs, widgets, routes — against the template's StateBase + CoreBlocBase + fl_theme conventions
Awareness index of every reusable widget in fl_ui, fl_theme, fl_media, and core's common_widget — name, one-line purpose, when to reach for it instead of writing a new one
Builds the data layer with Freezed DTOs, Retrofit clients, the storage-seam local data manager, and repositories wired through injectable
Teaches and applies Flutter/Dart dependency injection with Injectable + GetIt, grounded in this repo's Clean Architecture and code generation conventions. Use when changing DI wiring, adding BLoCs/use cases/repositories/modules, using @Named/@preResolve/@factoryParam/env registrations, reviewing DI best practices, or setting up DI tests.
| name | fl-bloc-pattern |
| description | Implements BLoC state management using CoreBlocBase, an abstract State hierarchy, and a freezed _StateData |
| license | MIT |
| metadata | {"audience":"flutter-developers","framework":"flutter","pattern":"bloc"} |
This template uses a specific bloc shape that is not the typical "freezed sealed union" pattern. The state hierarchy is hand-written abstract class siblings sharing a freezed _StateData; events are hand-written subclasses of an abstract event. Follow this shape exactly — state.copyWith<T>() and the _factories map depend on it.
CoreBlocBase<E, S> (defined in core/lib/presentation/base/bloc/bloc_base.dart). The template no longer ships a separate AppBlocBase layer — feature blocs inherit CoreBlocBase directly. If an app needs cross-cutting bloc behavior (analytics fan-out, common error mapping, feature-flag plumbing), it can reintroduce an AppBlocBase<E, S> extends CoreBlocBase<E, S> under apps/main/lib/presentation/base/ and have feature blocs extend that instead. Don't add the indirection pre-emptively — an empty layer is what was removed.@Injectable(). Use @factoryParam for runtime args.<X>Event — no freezed union.<X>State and share a freezed _StateData. The base provides copyWith<T extends <X>State>({_StateData? data}) backed by a _factories map._StateData is @freezed sealed class _StateData with _$StateData — generator declares it that way; do not change.package:core/core.dart (re-exports flutter_bloc, CoreBlocBase, helpers), package:freezed_annotation/freezed_annotation.dart, package:injectable/injectable.dart.make gen_all after editing — <feature>_bloc.freezed.dart is generated.<feature>_bloc.dart)import 'dart:async';
import 'package:core/core.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../../domain/usecases/<feature>/<feature>_usecase.dart';
part '<feature>_bloc.freezed.dart';
part '<feature>_event.dart';
part '<feature>_state.dart';
@Injectable()
class FeatureBloc extends CoreBlocBase<FeatureEvent, FeatureState> {
final FeatureUsecase _usecase;
FeatureBloc(
@factoryParam Item? initial,
this._usecase,
) : super(FeatureInitial(data: _StateData(initial: initial))) {
on<GetFeatureEvent>(_onGet);
}
Future<void> _onGet(GetFeatureEvent event, Emitter<FeatureState> emit) async {
final detail = await _usecase.getById(event.id);
emit(state.copyWith<FeatureInitial>(
data: state.data.copyWith(detail: detail),
));
}
}
<feature>_state.dart)// ignore_for_file: unused_element
part of '<feature>_bloc.dart';
@freezed
sealed class _StateData with _$StateData {
const factory _StateData({
Item? detail,
@Default([]) List<Item> items,
@Default(false) bool canLoadMore,
}) = __StateData;
}
abstract class FeatureState {
final _StateData data;
FeatureState(this.data);
T copyWith<T extends FeatureState>({_StateData? data}) {
return _factories[T == FeatureState ? runtimeType : T]!(
data ?? this.data,
);
}
// Forward common reads off _StateData.
Item? get detail => data.detail;
List<Item> get items => data.items;
bool get canLoadMore => data.canLoadMore;
}
class FeatureInitial extends FeatureState {
FeatureInitial({required _StateData data}) : super(data);
}
class FeatureLoaded extends FeatureState {
FeatureLoaded({required _StateData data}) : super(data);
}
final _factories = <Type, Function(_StateData data)>{
FeatureInitial: (data) => FeatureInitial(data: data),
FeatureLoaded: (data) => FeatureLoaded(data: data),
};
Add a new state class? Add it to _factories in the same edit — copyWith<T>() will throw at runtime otherwise.
<feature>_event.dart)part of '<feature>_bloc.dart';
abstract class FeatureEvent {}
class GetFeatureEvent extends FeatureEvent {
final String id;
GetFeatureEvent(this.id);
}
class LoadMoreEvent extends FeatureEvent {}
For events that need to surface a result back to the caller (e.g. login flows), don't stash a Completer<T> on the event — model the outcome as concrete state subclasses (LoginSuccess, LoginFailed) and let the screen's _blocListener react. The signin module is the canonical example. Reserve the completer pattern for callers that must await a single-shot side effect outside the BLoC stream (e.g. a coordinator chain), and even there prefer returning the refreshed domain object from a use case over a bool.
Guard re-entry at the top of long-running handlers when the success state is terminal (if (state is LoginSuccess) return;). This keeps double-taps idempotent without locking the bloc with extra fields.
StateBase<T> (the screen base) wires delegate?.addLoadingHandler and addErrorHandler against the bloc's CoreDelegate mixin during initState. So a bloc generally does not call showLoading/hideLoading itself — the screen does, and CoreBlocBase.onError already routes thrown errors through the screen's error UI.
When you do need explicit control inside a handler:
Future<void> _onSubmit(SubmitEvent event, Emitter<FeatureState> emit) async {
showLoading(); // CoreDelegate fan-out — wakes the screen's EasyLoading
try {
await _usecase.submit(event.payload);
emit(state.copyWith<FeatureSuccess>());
} finally {
hideLoading();
}
}
showLoading() / hideLoading() here are the CoreDelegate versions
inherited via CoreBlocBase. They notify every registered screen
(usually one) which then calls its own showLoading() / hideLoading()
on EasyLoading. There's a deliberate name collision with the screen's
methods — see fl-error-handling for the full picture.
Throw or rethrow on errors — CoreBlocBase.onError converts them to ErrorData and forwards to the screen.
BlocBuilder works against the runtime type:
BlocBuilder<FeatureBloc, FeatureState>(
builder: (context, state) {
if (state is FeatureLoaded) return _buildList(state.items);
return _buildEmpty();
},
)
Action files (see fl-extension-action) typically use _blocListener(BuildContext context, FeatureState state) for side effects and dispatch events via bloc.add(...).
CoreBlocBase<E, S> and is @Injectable().<feature>_bloc.dart declares the three part directives (freezed, event, state)._StateData is @freezed sealed class and uses @Default(...) for non-nullable defaults._factories.<X>Event; no freezed unions for events or states.showLoading() and pair it with hideLoading() in a finally block (CoreDelegate fan-out).make gen_all run after changes.FeatureState as a freezed union (FeatureState.loading()/.loaded()) — this template does not._factories (causes Null check operator used on a null value from copyWith<T>).package:flutter_bloc/flutter_bloc.dart directly — go through package:core/core.dart.emit(...error) and rethrow — CoreBlocBase.onError already handles thrown errors; emit OR throw, don't double-fire.