Help us improve
Share bugs, ideas, or general feedback.
From ufil
GM Bloc state management patterns — part/part-of structure, sub-state unions (idle/loading/error/done), modular (Failure) vs non-modular (Result switch)
npx claudepluginhub ghozimahdi/gm-claude-plugins --plugin ufilHow this skill is triggered — by the user, by Claude, or both
Slash command
/ufil:bloc-patternThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The bloc file is the main library. Event and state files use `part of`:
Creates p5.js generative art with seeded randomness, noise fields, and interactive parameter exploration. Use for algorithmic art, flow fields, or particle systems.
Share bugs, ideas, or general feedback.
part / part of (3 files per bloc)The bloc file is the main library. Event and state files use part of:
blocs/
└── property_detail/
├── property_detail_bloc.dart ← main file (has part directives)
├── property_detail_event.dart ← part of bloc
└── property_detail_state.dart ← part of bloc
@freezed abstract class, part of bloc// property_detail_event.dart
part of 'property_detail_bloc.dart';
@freezed
abstract class PropertyDetailEvent with _$PropertyDetailEvent {
const factory PropertyDetailEvent.init({required String propertyId}) = _InitEvent;
const factory PropertyDetailEvent.refresh() = _RefreshEvent;
const factory PropertyDetailEvent.delete() = _DeleteEvent;
}
Failure// property_detail_state.dart
part of 'property_detail_bloc.dart';
@freezed
abstract class PropertyDetailState with _$PropertyDetailState {
const factory PropertyDetailState({
@Default(GetPropertyDetailState.idle()) GetPropertyDetailState propertyDetailState,
@Default(DeletePropertyDetailState.idle()) DeletePropertyDetailState deletePropertyDetailState,
}) = _PropertyDetailState;
}
@freezed
abstract class GetPropertyDetailState with _$GetPropertyDetailState {
const factory GetPropertyDetailState.idle({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = _GetPropertyDetailIdleState;
const factory GetPropertyDetailState.loading({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = GetPropertyDetailLoadingState;
const factory GetPropertyDetailState.done({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = GetPropertyDetailDoneState;
const factory GetPropertyDetailState.error({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = GetPropertyDetailErrorState;
}
@freezed
abstract class DeletePropertyDetailState with _$DeletePropertyDetailState {
const factory DeletePropertyDetailState.idle({
@Default(Failure.noFailure()) Failure failure
}) = _DeletePropertyDetailIdleState;
const factory DeletePropertyDetailState.loading({
@Default(Failure.noFailure()) Failure failure
}) = DeletePropertyDetailLoadingState;
const factory DeletePropertyDetailState.done({
@Default(Failure.noFailure()) Failure failure
}) = DeletePropertyDetailDoneState;
const factory DeletePropertyDetailState.error({
@Default(Failure.noFailure()) Failure failure
}) = DeletePropertyDetailErrorState;
}
Failure object// property_detail_state.dart
part of 'property_detail_bloc.dart';
@freezed
abstract class PropertyDetailState with _$PropertyDetailState {
const factory PropertyDetailState({
@Default(GetPropertyDetailState.idle()) GetPropertyDetailState propertyDetailState,
@Default(DeletePropertyDetailState.idle()) DeletePropertyDetailState deletePropertyDetailState,
}) = _PropertyDetailState;
}
@freezed
abstract class GetPropertyDetailState with _$GetPropertyDetailState {
const factory GetPropertyDetailState.idle({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = _GetPropertyDetailIdleState;
const factory GetPropertyDetailState.loading({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = GetPropertyDetailLoadingState;
const factory GetPropertyDetailState.done({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = GetPropertyDetailDoneState;
const factory GetPropertyDetailState.error({
@Default(PropertyModel()) PropertyModel property,
@Default(Failure.noFailure()) Failure failure
}) = GetPropertyDetailErrorState;
}
@freezed
abstract class DeletePropertyDetailState with _$DeletePropertyDetailState {
const factory DeletePropertyDetailState.idle({
@Default(Failure.noFailure()) Failure failure
}) = _DeletePropertyDetailIdleState;
const factory DeletePropertyDetailState.loading({
@Default(Failure.noFailure()) Failure failure
}) = DeletePropertyDetailLoadingState;
const factory DeletePropertyDetailState.done({
@Default(Failure.noFailure()) Failure failure
}) = DeletePropertyDetailDoneState;
const factory DeletePropertyDetailState.error({
@Default(Failure.noFailure()) Failure failure
}) = DeletePropertyDetailErrorState;
}
Carrying Failure preserves type information so the UI can switch on failure type (e.g. show retry for NoConnectionFailure, redirect for UnauthorizedFailure).
| Part | Pattern | Example |
|---|---|---|
| Sub-state class | {Action}{BlocName}State | GetPropertyDetailState, DeletePropertyDetailState |
.idle() variant | _ prefix (private) | _GetPropertyDetailIdleState |
.loading() variant | No prefix (public) | GetPropertyDetailLoadingState |
.done() variant | No prefix (public) | GetPropertyDetailDoneState |
.error() variant | No prefix (public) | GetPropertyDetailErrorState |
| State field name | {blocName}State or {action}{BlocName}State | propertyDetailState, deletePropertyDetailState |
Result<T>// property_detail_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
part 'property_detail_bloc.freezed.dart';
part 'property_detail_event.dart';
part 'property_detail_state.dart';
@injectable
class PropertyDetailBloc extends Bloc<PropertyDetailEvent, PropertyDetailState> {
PropertyDetailBloc(this._getPropertyDetail, this._deleteProperty)
: super(const PropertyDetailState()) {
on<_InitEvent>(_initEvent);
on<_DeleteEvent>(_deleteEvent);
}
final GetPropertyDetailUsecase _getPropertyDetail;
final DeletePropertyUsecase _deleteProperty;
Future<void> _initEvent(_InitEvent event, Emitter<PropertyDetailState> emit) async {
emit(
state.copyWith(
propertyDetailState: const GetPropertyDetailState.loading(),
),
);
final result = await _getPropertyDetail(propertyId: event.propertyId);
switch (result) {
case Ok(:final value):
emit(
state.copyWith(
propertyDetailState: GetPropertyDetailState.done(property: value),
),
);
case Error(:final error):
emit(
state.copyWith(
propertyDetailState: GetPropertyDetailState.error(failure: error),
),
);
}
}
Future<void> _deleteEvent(_DeleteEvent event, Emitter<PropertyDetailState> emit) async {
emit(state.copyWith(deletePropertyDetailState: const DeletePropertyDetailState.loading()));
final result = await _deleteProperty(propertyId: event.propertyId);
switch (result) {
case Ok():
emit(state.copyWith(deletePropertyDetailState: const DeletePropertyDetailState.done()));
case Error(:final error):
emit(
state.copyWith(
deletePropertyDetailState: DeletePropertyDetailState.error(failure: error),
),
);
}
}
}
switch on returned Failure// packages/presentation/feature_property/lib/src/blocs/property_detail/property_detail_bloc.dart
import 'package:domain_common/domain_common.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
part 'property_detail_bloc.freezed.dart';
part 'property_detail_event.dart';
part 'property_detail_state.dart';
@injectable
class PropertyDetailBloc extends Bloc<PropertyDetailEvent, PropertyDetailState> {
PropertyDetailBloc(this._getPropertyDetail, this._deleteProperty)
: super(const PropertyDetailState()) {
on<_InitEvent>(_initEvent);
on<_DeleteEvent>(_deleteEvent);
}
final GetPropertyDetailUsecase _getPropertyDetail;
final DeletePropertyUsecase _deleteProperty;
Future<void> _initEvent(_InitEvent event, Emitter<PropertyDetailState> emit) async {
emit(
state.copyWith(
propertyDetailState: const GetPropertyDetailState.loading(),
),
);
final result = await _getPropertyDetail(propertyId: event.propertyId);
switch (result.failure) {
case NoFailure():
emit(
state.copyWith(
propertyDetailState: GetPropertyDetailState.done(property: result.property),
),
);
default:
emit(
state.copyWith(
propertyDetailState: GetPropertyDetailState.error(
failure: result.failure,
),
),
);
}
}
Future<void> _deleteEvent(_DeleteEvent event, Emitter<PropertyDetailState> emit) async {
emit(state.copyWith(deletePropertyDetailState: const DeletePropertyDetailState.loading()));
final failure = await _deleteProperty(propertyId: event.propertyId);
switch (failure) {
case NoFailure():
emit(state.copyWith(deletePropertyDetailState: const DeletePropertyDetailState.done()));
default:
emit(
state.copyWith(
deletePropertyDetailState: DeletePropertyDetailState.error(failure: failure),
),
);
}
}
}
BlocProvider(
create: (_) => getIt<PropertyDetailBloc>()
..add(PropertyDetailEvent.init(propertyId: propertyId)),
child: const PropertyDetailPage(),
)
All UI state (loading, toggle, page index, etc.) must go through Bloc events/states. Exception: Flutter controllers (TextEditingController, PageController, ScrollController, FocusNode, AnimationController, GlobalKey) are OK as local fields.
Padding(padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h))
SizedBox(height: 16.h)
Icon(Icons.home, size: 24.sp)
BorderRadius.circular(12.r)
TextStyle(fontSize: 14.sp)
Use PagingState in Bloc + PagedSliverList/PagedSliverGrid in UI. Never manual isLoading/hasMore flags.
BlocListener<PropertyDetailBloc, PropertyDetailState>(
listenWhen: (prev, curr) => prev.deletePropertyDetailState != curr.deletePropertyDetailState,
listener: (context, state) {
switch (state.deletePropertyDetailState) {
case DeletePropertyDetailDoneState():
Navigator.of(context).pop();
case DeletePropertyDetailErrorState(:final failure):
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(failure.toString())));
default:
break;
}
},
)
Failure type for granular handlingBlocListener<PropertyDetailBloc, PropertyDetailState>(
listenWhen: (prev, curr) => prev.deletePropertyDetailState != curr.deletePropertyDetailState,
listener: (context, state) {
switch (state.deletePropertyDetailState) {
case DeletePropertyDetailDoneState():
context.router.maybePop();
case DeletePropertyDetailErrorState(:final failure):
switch (failure) {
case NoConnectionFailure():
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No internet connection. Please try again.')),
);
case UnauthorizedFailure():
context.router.replaceAll([const LoginRoute()]);
default:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(failure.toString())),
);
}
default:
break;
}
},
)
@injectable (factory)@lazySingleton (shared)part/part of structure — event and state files are part of bloc file@freezed abstract class for events, states, sub-states (not sealed class){Action}{BlocName}State with .idle(), .loading(), .done(), .error().idle() variant is private (_ prefix), .loading()/.done()/.error() are public.init() → _InitEvent → _initEvent handler_ kept, first letter lowercased, Event suffix kept). _GetTransactionEvent → _getTransactionEvent, _SubmittedEvent → _submittedEvent. NEVER _onX / _onXEvent / _handleX.switch on Result in event handlers; query usecases return Future<Result<T>>Future<Result> (Result carries data + Failure); action usecases return Future<Failure>. switch on result.failure / returned Failure (NoFailure = success).idle()/.loading()/.done()/.error()) share the SAME named params with defaults — @Default(Failure.noFailure()) Failure failure + optional @Default(...) {DataType} {field} for queries. The state class itself is uniform; only the call sites in the bloc are minimal.loading uses the const default constructor (const GetState.loading()); done passes ONLY the new data (GetState.done(property: value)); error passes ONLY failure (GetState.error(failure: failure)). Do NOT preserve previous data on loading/error; the field's @Default(...) already provides a safe fallback.context.read<TBloc>().add(event) and reads via BlocBuilder/BlocConsumer/BlocSelector/BlocListener. A page that imports a UseCase or calls getIt<XUseCase>() is ALWAYS wrong, with no exceptions.${CLAUDE_PLUGIN_ROOT}/docs/BLOC_PATTERN.md — Detailed Bloc patterns and conventions${CLAUDE_PLUGIN_ROOT}/docs/PRESENTATION_LAYER.md — Presentation layer structure