flutter-ui-side-effects
Skill10 starsChanged 42 days agoMITView on GitHub
Use when a Flutter widget needs navigation, snackbars, dialogs, analytics or any one-shot reaction to a state change, when button callbacks contain context.router / ScaffoldMessenger / analytics calls, when error copy must be shown for a failed action, or when adding a <Feature>Listeners widget. Covers ref.listen listener widgets, per-attempt submission state and mapping failures to user-facing text.
In this repository5 other files it gives its agents.
- flutter-feature-scaffoldSkill
- flutter-result-errorsSkill
- flutter-riverpod3Skill
- flutter-sdk-contractsSkill
- flutter-testingSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
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 120
Two listeners racing for the navigation stack: If a session listener and a setup listener can both navigate on the same change, make one …
- Very specific: line 123
After an
await, checkcontext.mounted: before using context. - Distinctive: line 124
Order inside one widget is declaration order: Do not rely on it between listener widgets.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
UI side effects with listener widgets
Side effects (navigation, snackbars, dialogs, analytics) are never
performed inline in button callbacks or scattered through widgets. They are
declared with ref.listen, grouped in one listener widget per feature
placed near the page root.
Why
- One place to read. "What happens after sign-in?" has one answer, not six
onPressedhandlers. - Reacts to the real outcome. A callback that navigates right after
submit()runs before the request has succeeded. A listener fires when the state actually becomes success or error. - No rebuilds. The widget listens without watching and returns
childuntouched, so it never rebuilds the page beneath it. - Testable. A listener test mounts only the listener, overrides providers,
drives state and asserts on a recording fake (see
flutter-testing).
The pattern
Template: assets/feature_listeners.dart.
class ItemsListeners extends ConsumerWidget with FailureMessageMapper {
const ItemsListeners({required this.child, super.key});
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(itemSubmissionProvider, (previous, next) {
switch (next) {
case AsyncData(value: final result?): // success
...
case AsyncError(error: final AppFailure failure): // failure
AppSnackBar.show(context, message: failureMessage(context.l10n, failure));
default:
break;
}
});
return child;
}
}Pages wrap their Scaffold:
return ItemsListeners(child: Scaffold(...));Rules
- One listener widget per feature,
<Feature>Listeners, underfeatures/<feature>/presentation/widgets/. - Each effect listens to the state it semantically follows. Navigation follows the session; error copy follows the attempt; analytics follow whichever state they measure. Do not hang everything off one provider.
ref.listen, notref.watch, inside the listener widget, and returnchildunchanged.- Transient action state lives in its own
autoDisposeprovider (AsyncValue<T?>, idle =AsyncData(null)), never in the app-wide session provider and never insetState. UseSubmissionNotifierfromflutter-result-errors. - Widgets read the same provider for declarative state
(
submission.isLoadingdrives a button'sloadingflag); the listener owns the imperative reactions. - User feedback goes through your design-system wrapper (
AppSnackBar), never rawSnackBars sprinkled around. - Copy comes from l10n keyed on the failure type via a
FailureMessageMapper, not fromfailure.message. Template:assets/failure_message_mapper.dart. - Listeners react; they do not decide. Business rules stay in the domain
or the notifier. A listener that grows
ifladders about business state is a missing domain method.
Reading an AsyncValue in a listener
Pattern-match, most specific first:
switch (next) {
case AsyncData(value: final result?): // success
...
case AsyncError(error: final AppFailure f) when isInline(f): // shown by the form
break;
case AsyncError(error: final AppFailure f): // everything else
...
default: // idle / loading
break;
}Skip failures another widget already presents (field validation) and
CancelledFailure (the user backed out; staying silent is the right UX).
Reacting to changes, not emissions
A listener can fire for a rebuild that changed nothing relevant. Compare
previous and next:
ref.listen(itemsProvider, (previous, next) {
final topic = next.value?.topic;
if (topic == null || topic == previous?.value?.topic) return; // no change
ref.read(analyticsServiceProvider).track(ItemsFilteredEvent(topic: topic));
});Gotchas
- A widget above the router has no
context.router. A global listener inMaterialApp.buildermust reach the router through a provider (ref.read(appRouterProvider)), not throughcontext. - Two listeners racing for the navigation stack. If a session listener and a setup listener can both navigate on the same change, make one stand aside explicitly and write down why.
- After an
await, checkcontext.mountedbefore usingcontext. - Order inside one widget is declaration order. Do not rely on it between listener widgets.
ref.listendoes not fire for the initial value (unlessfireImmediately: true). Use that deliberately, rarely.- Do not
ref.watchinside the listener to read a value you need in the callback; useref.readin the callback.
Review checklist
- No
Navigator,context.router,ScaffoldMessengeror analytics call in anonPressed - One
<Feature>Listenersper feature, wrapping the page, returningchild - Each
ref.listenfollows the state its effect depends on - Action state is an
autoDisposesubmission provider, not session state orsetState - Error copy keyed on failure type through the mapper, shown via the design-system snackbar
CancelledFailureand inline-handled failures stay silent- Repeated emissions are de-duplicated by comparing
previousandnext
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.

