state-management
Skill0 starsChanged 51 days agoMITView on GitHub
Explains when to use a plain Cubit vs a full Bloc with events, how to shape a state class (status enum + copyWith vs a sealed hierarchy), the Screen/View split for wiring a Cubit into a widget tree, and the app-wide-vs-screen-scoped Cubit distinction; triggers whenever a new Cubit/BLoC or its state class is being designed.
27 other projects use a version of this.
Adopted by
Most recently taken up by
- komluk/scaffoldingtheir own version, first seen Nov 7, 2026
- compozy/compozytheir own version, first seen Nov 7, 2026
- omspradippatil/Claude-Skillstheir own version, first seen Nov 1, 2026
- github/replicatheir own version, first seen Nov 1, 2026
- darknecrocities/SkillBuildertheir own version, first seen Nov 1, 2026
- TheBeardedBearSAS/claude-crafttheir own version, first seen Oct 29, 2026
Used with
Projects with it also use css-architecture, i18n and 8 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
Piyush8296Skillcss-architecture
CSS architecture, Tailwind patterns, responsive design, design tokens, and styling best practices. Use when implementing designs, setting up styling infrastructure, or debugging layout issues.
Also in 3 projects that use this
lbednerSkilli18n
Use when a code or template change introduces a new translation message key, in either the framework CLI (aegis/i18n/locales/) or a generated project's CLI (the app/i18n/locales/ template tree). Covers adding the real English string, stubbing the same key into every other locale for parity, and the separate translation PR that later replaces placeholders with reviewed native text.
Also in 4 projects that use this
mijunriSkilldatabase
数据库脚本与操作规范。脚本放在 db_scripts 目录,遵循查询优化、事务、安全等实践。
Also in 3 projects that use this
genomewalkerSkillresume
Resume a thread by loading its ~800-token context capsule
Also in 3 projects that use this
0xSeroSkillsecurity
Also in 4 projects that use this
klauswgSkillperformance
Optimize web performance for faster loading and better user experience. Use when asked to "speed up my site", "optimize performance", "reduce load time", "fix slow loading", "improve page speed", or "performance audit".
Also in 3 projects that use this
Other versions27 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 27
- compozy/compozy★ 2.9ksame name
- github/replica★ 230same name
- conorluddy/xclaude-plugin★ 186same name
- cohen-liel/hivemind★ 108same name
- TheBeardedBearSAS/claude-craft★ 108same name
- EmeaAppGbb/spec2cloud★ 100same name
- statelyai/skills★ 48same name
- komluk/scaffolding★ 16same name
- darknecrocities/SkillBuilder★ 13same name
- PMDevSolutions/Aurelius★ 9same name
17 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository35 other files it gives its agents.
- AGENTS.mdAGENTS.md
- bootstrap-projectSkill
- choose-data-stackSkill
- code-reviewSkill
- conventional-commitsSkill
- data-providerSkill
- domain-modelingSkill
- error-handlingSkill
- flavors-and-flagsSkill
- flutter-fvmSkill
- localizationSkill
- new-featureSkill
- quality-gateSkill
- routingSkill
- testingSkill
- themingSkill
- ui-screenSkill
- bootstrap-projectSkill
- choose-data-stackSkill
- code-reviewSkill
- conventional-commitsSkill
- data-providerSkill
- domain-modelingSkill
- error-handlingSkill
- flavors-and-flagsSkill
- flutter-fvmSkill
- flutter_fvmSkill
- localizationSkill
- new-featureSkill
- quality-gateSkill
- routingSkill
- state-managementSkill
- testingSkill
- themingSkill
- ui-screenSkill
The file
We split this skill into the rules it gives an agent
3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.
0
Common
Most of the 21 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 19
Use a Cubit for the large majority of screens: a method per user action, each one loading data, mapping a failure, or updating a field.
- Distinctive: line 87
If you find yourself reaching for a Bloc just to have named events with no second trigger source, use a Cubit instead — it is less ceremony …
- Very specific: line 257
Don't wrap a screen in a second BlocProvider<FeatureFlagCubit> — that would shadow the app-wide instance with a fresh one that never …
“Common” is measured: kept by at least 6 in 10 of the 21 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
State management: Cubit vs Bloc, and state shape
This template uses flutter_bloc for all state management (root AGENTS.md
section 3). setState inside a feature screen is forbidden. This skill
covers the two decisions you make every time you add state: Cubit vs
Bloc, and how to shape the state class.
1. Cubit vs Bloc: default to Cubit
A Cubit exposes plain methods that call emit directly. A Bloc
funnels every trigger through on<Event> handlers dispatched from a single
add(event) call.
Use a Cubit for the large majority of screens: a method per user action, each one loading data, mapping a failure, or updating a field.
class TaskListCubit extends Cubit<TaskListState> {
TaskListCubit(this._repository) : super(const TaskListState.initial());
final TaskRepository _repository;
Future<void> load() async {
emit(state.copyWith(status: TaskListStatus.loading));
try {
final tasks = await _repository.getAll();
if (isClosed) return;
emit(state.copyWith(status: TaskListStatus.loaded, tasks: tasks));
} on DomainFailure catch (failure) {
if (isClosed) return;
emit(state.copyWith(status: TaskListStatus.error, error: _mapFailure(failure)));
}
}
}Reach for a full Bloc only when you genuinely need several distinct
triggers merged into one ordered stream of state changes — not merely
"more than one method". The signal is: two or more independent event
sources (a user tap, a repository stream, a timer) that must be
serialized through the same state machine, or a caller elsewhere in the
codebase needs to dispatch semantically named events (analytics, event
replay, bloc_concurrency transformers) rather than call a method
directly. A common concrete case is combining a widget-driven event with
a repository Stream via emit.onEach inside an event handler:
class TaskListBloc extends Bloc<TaskListEvent, TaskListState> {
TaskListBloc(this._repository) : super(const TaskListState.initial()) {
on<TaskListSubscriptionRequested>(_onSubscriptionRequested);
on<TaskListFilterChanged>(_onFilterChanged);
}
final TaskRepository _repository;
StreamSubscription<List<Task>>? _subscription;
Future<void> _onSubscriptionRequested(
TaskListSubscriptionRequested event,
Emitter<TaskListState> emit,
) async {
emit(state.copyWith(status: TaskListStatus.loading));
await emit.onEach<List<Task>>(
_repository.watchAll(),
onData: (tasks) => emit(state.copyWith(status: TaskListStatus.loaded, tasks: tasks)),
onError: (error, stackTrace) =>
emit(state.copyWith(status: TaskListStatus.error, error: _mapFailure(error))),
);
}
void _onFilterChanged(TaskListFilterChanged event, Emitter<TaskListState> emit) {
emit(state.copyWith(filter: event.filter));
}
@override
Future<void> close() {
_subscription?.cancel();
return super.close();
}
}If you find yourself reaching for a Bloc just to have named events with no second trigger source, use a Cubit instead — it is less ceremony for the same outcome and is what most of this codebase's screens need.
2. Shaping the state class
The majority case: a single class with a status enum + copyWith
Most screens have one state shape whose fields simply become
populated/empty at different times (loading, loaded, error are all "the
same screen, different data"). Model this as a single Equatable class
with a status enum, mirroring the copyWith-with-sentinel convention used
for entities (packages/domain/lib/src/entities/entities.dart):
enum TaskListStatus { initial, loading, loaded, error }
class TaskListState extends Equatable {
const TaskListState({
this.status = TaskListStatus.initial,
this.tasks = const [],
this.error,
});
const TaskListState.initial() : this();
final TaskListStatus status;
final List<Task> tasks;
final TaskListError? error;
TaskListState copyWith({TaskListStatus? status, List<Task>? tasks, TaskListError? error}) =>
TaskListState(
status: status ?? this.status,
tasks: tasks ?? this.tasks,
// Explicitly nulled by the caller when re-entering a non-error status,
// not accidentally retained from a previous emit — see below.
error: error,
);
@override
List<Object?> get props => [status, tasks, error];
}Note the error field: a copyWith that does error ?? this.error would
make an error "sticky" across a subsequent successful load, because
null (the caller's intent: "clear it") is indistinguishable from
"unchanged". Either pass error through unconditionally (as above, always
supplying the new value explicitly at every call site) or use the
sentinel pattern documented in entities.dart if the state has several
independently-clearable nullable fields.
FeatureFlagState (packages/ui/lib/src/app_blocs/feature_flag_state.dart)
is the trivial end of this spectrum — no status enum at all, because it has
exactly one shape and no loading/error phase. Don't copy its brevity for a
screen that genuinely has multiple phases; the fuller shape above is the
right default.
The sealed-hierarchy case: genuinely distinct shapes
Reach for a sealed class hierarchy only when different states carry
different fields, not just different values of the same fields — e.g. a
form draft state that needs an editingTaskId only while editing, and a
list state that has no such concept:
sealed class TaskFormState extends Equatable {
const TaskFormState();
}
final class TaskFormEditing extends TaskFormState {
const TaskFormEditing({required this.title, this.error});
final String title;
final ValidationError? error;
@override
List<Object?> get props => [title, error];
}
final class TaskFormSubmitting extends TaskFormState {
const TaskFormSubmitting(this.title);
final String title;
@override
List<Object?> get props => [title];
}
final class TaskFormSubmitted extends TaskFormState {
const TaskFormSubmitted(this.taskId);
final String taskId;
@override
List<Object?> get props => [taskId];
}The widget then uses an exhaustive switch instead of reading a status
field, and the analyzer catches a forgotten branch when a new subtype is
added — the same tradeoff DomainFailure makes (root AGENTS.md section
9). Default to the status-enum shape; only split into a sealed hierarchy
once a single shared field set genuinely stops fitting every phase.
3. The Screen/View split
A screen that owns its own Cubit is split into a public XScreen (creates
the Cubit) and a private _XView (consumes it):
class TaskListScreen extends StatelessWidget {
const TaskListScreen({super.key});
@override
Widget build(BuildContext context) => BlocProvider(
create: (context) => TaskListCubit(context.read<TaskRepository>())..load(),
child: const _TaskListView(),
);
}
class _TaskListView extends StatelessWidget {
const _TaskListView();
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(context.l10n.taskListTitle)),
body: BlocBuilder<TaskListCubit, TaskListState>(
builder: (context, state) => _buildBody(context, state),
),
);
Widget _buildBody(BuildContext context, TaskListState state) => switch (state.status) {
TaskListStatus.initial || TaskListStatus.loading => const Center(
child: CircularProgressIndicator(),
),
TaskListStatus.error => _ErrorView(error: state.error!),
TaskListStatus.loaded => _TaskListContent(tasks: state.tasks),
};
}context.read<TaskRepository>() is used inside create: (a one-shot
read, no rebuild dependency); BlocBuilder is used inside the view (a
rebuild dependency). Never call context.read<TaskListCubit>() for
values you need to rebuild on — that's what BlocBuilder/context.watch
is for.
App-wide Cubit vs screen-scoped Cubit
Most Cubits are screen-scoped: created locally inside that screen's
own BlocProvider, as above. A Cubit is app-wide only when more than
one screen genuinely needs the same live state — it is provided once,
above MaterialApp.router, in lib/app.dart:
// lib/app.dart
BlocProvider(create: (context) => FeatureFlagCubit(context.read<FeatureFlagRepository>()))A screen that only reads an app-wide Cubit skips the BlocProvider step
entirely and goes straight to BlocBuilder — see
packages/ui/lib/src/features/home/presentation/home_screen.dart:
Widget _buildBody(BuildContext context) => BlocBuilder<FeatureFlagCubit, FeatureFlagState>(
builder: (context, state) => Text(context.l10n.homeScreenPlaceholder),
);Don't wrap a screen in a second BlocProvider<FeatureFlagCubit> — that
would shadow the app-wide instance with a fresh one that never receives
the app's real feature-flag data. If you're not sure which category a new
Cubit falls into, default to screen-scoped; promote to app-wide only once
a second screen needs it (the same YAGNI rule common_widgets/ follows).
4. Three correctness rules, every time
Guard async emits. Between an
awaitand anemit, the Cubit may already be closed (the screen was popped mid-request). Always check first:final tasks = await _repository.getAll(); if (isClosed) return; emit(state.copyWith(tasks: tasks));Cancel stream subscriptions in
close(). AnyStreamSubscriptionstarted in the Cubit/Bloc (awatch()repository stream, a timer) must be cancelled, or it keeps firing (and potentially throws on a closed Cubit) after the widget is gone:@override Future<void> close() { _subscription?.cancel(); return super.close(); }Guard
BuildContextafter anawaitin the widget layer. This is the UI-side mirror of the two rules above — after anyawaitin a callback that touchescontext(e.g.context.read,Navigator,ScaffoldMessenger), checkmountedfirst:onPressed: () async { await context.read<TaskListCubit>().delete(taskId); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(/* ... */); }
Checklist
- Chose Cubit unless there are genuinely multiple distinct event
sources that need merging through
on<Event>/emit.onEach. - State class is a single
Equatablewith a status enum +copyWithunless different phases need genuinely different fields. copyWithdoes not make a nullable field (e.g.error) sticky across an unrelatedemit.- Public
XScreencreates the Cubit viaBlocProvider; private_XViewconsumes it viaBlocBuilder. - Confirmed whether this Cubit is screen-scoped (default) or app-wide
(provided once in
lib/app.dart) — and did not re-wrap an app-wide Cubit in a localBlocProvider. if (isClosed) return;before every asyncemit.- Every
StreamSubscriptionis cancelled inclose(). if (!context.mounted) return;after everyawaitthat precedesBuildContextuse.- A test exists for the Cubit/Bloc (see
.agents/skills/testing/SKILL.md).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

