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.