Clean Flutter
Flutter and Dart conventions for writing, reviewing, and refactoring. The core rules below always apply. Load the one reference file matching the task's domain; don't load them all.
If the clean-code skill is installed, its language-agnostic principles (function size, naming hygiene, comment discipline) still apply. Where generic advice conflicts with Flutter idiom, this skill wins: "replace switch with polymorphism" maps to exhaustive switch over a sealed class, which in Dart is the intended tool, not a smell.
| Task touches | File |
|---|---|
Project layout, MVVM layering, repository boundaries, Result |
references/architecture.md |
Providers, Notifier/AsyncNotifier, ref.watch/read/listen, DI, autoDispose |
references/state-management.md |
| Routes, deep links, nested or guarded navigation | references/navigation.md |
| Data models, freezed, JSON serialization, build_runner workflow | references/models-serialization.md |
| Writing tests: unit, widget, golden, integration | references/testing.md |
Rebuild storms, async-gap crashes, layout overflow, dynamic leaks |
references/pitfalls.md |
| Picking a package (storage, network, forms, i18n, background), pubspec hygiene | references/packages.md |
Toolchain & Setup
- Pin the Flutter SDK with fvm (
.fvmrc,fvm use) and commit the pin so every machine and CI build the same stable SDK. very_good_analysisinanalysis_options.yamlas the lint floor. It layersstrict-casts,strict-raw-types, andstrict-inferenceon top offlutter_lints, catching the exact classes of mistake generated code produces (implicitdynamic, unchecked casts). Fix warnings; never loosen a rule to silence one.flutter analyzestays clean in CI.- Codegen (
freezed,json_serializable,riverpod_generator,retrofit) runs throughdart run build_runner build --delete-conflicting-outputs. Generated*.g.dartand*.freezed.dartare build output: regenerate after editing the annotated source, never hand-edit them. Detail inreferences/models-serialization.md.
Widgets & Build
build()is pure: no side effects, no I/O, no allocation you can hoist out. It runs often; assume every rebuild is free to happen at any time.constwherever the analyzer allows (prefer_const_constructors). Aconstwidget is skipped on rebuild, the cheapest perf win and the one generated code drops most.- Extract widget subtrees into their own
StatelessWidget/ConsumerWidgetclasses, notWidget _buildFoo()methods. A helper method rebuilds with its parent and can't beconst; a widget class gets its own rebuild boundary and const-ness. - Compose small widgets. A deep
buildmethod that could be five classes is the Flutter equivalent of a 200-line function. Keys only where identity matters (reordered or inserted list items, state preserved across a position change). Don't scatter keys nothing reads.
State
- Riverpod with codegen (
@riverpod) is the default for both state and DI.ref.watchinbuildto react to changes;ref.readin callbacks and event handlers for a one-shot read. Watching in a callback re-subscribes on every call; reading inbuildmisses updates. Detail inreferences/state-management.md. - Push state to the smallest widget that needs it. Scope
ref.watchwith.select(...)so a one-field change doesn't rebuild the whole screen. setStateis fine for genuinely local ephemeral state (a toggle, focus, an animation flag). Not for app state or anything shared across widgets.- Package-specific deprecation/trend verdicts (GetX, Isar, old Hive, golden_toolkit) live in
references/pitfalls.md's Named traps list, even when the package's own topic has a dedicated reference file (e.g.state-management.md).
Async & Lifecycle
- After any
awaitinside aStateor callback, aBuildContextmay be dead. Guard withif (!context.mounted) return;(orif (!mounted) return;in aState) before touchingcontext,Navigator,setState, or a controller.use_build_context_synchronouslyflags the common cases, not all of them. - Dispose what you create.
TextEditingController,ScrollController,AnimationController,FocusNode, andStreamSubscriptionall leak and can fire after teardown without a matchingdispose()/cancel().
Errors
- Repositories catch throwing calls (network, disk, platform channels) internally and return a sealed
Result<T>(Ok/Failure); callersswitchon it exhaustively. The failure path becomes part of the signature instead of an invisible throw. Detail inreferences/architecture.md. - Let genuine bugs (programmer error, broken preconditions) throw and reach crash reporting. Don't wrap them in a
try/catchthat swallows them into a silent no-op.
Naming & Style
lowerCamelCasefor members and locals,UpperCamelCasefor types,lowercase_with_underscoresfor files and directories. Matchdart format; never hand-format around it.- Private by default (
_name); widen visibility only when another library needs it. async/awaitover raw.then()chains. Type every public signature and let strict-inference reject accidentaldynamic.
Pre-Submit Checklist
-
flutter analyzeclean under very_good_analysis; no rule loosened to pass - codegen regenerated after any model/provider edit; no hand-edited
*.g.dart/*.freezed.dart -
conston every eligible widget; subtrees extracted to classes, not_buildmethods - no
BuildContext/Navigator/setStateafter anawaitwithout amountedguard - every controller, subscription, and notifier disposed
-
ref.watchin build,ref.readin callbacks,ref.listenfor side effects; watch scoped with.select - repository calls return
Result; genuine bugs throw rather than being swallowed - no unmaintained package added (pub.dev signals checked, see
references/packages.md) - tests fail if the logic breaks; layer chosen per the pyramid (
references/testing.md)