flutter-dart-style
Skill12 starsChanged 56 days agoMITView on GitHub
Use when writing or reviewing any Dart or Flutter code for craft - naming, null safety, pattern matching, records, error handling idioms, dartdoc comments, logging, assertion style in tests, widget-level code quality (const, keys, dispose, BuildContext across an await), and analysis_options lint setup. Triggers on "clean this up", "is this idiomatic?", a code review of Dart, or a new file that sets a precedent others will copy.
In this repository15 other files it gives its agents.
- flutter-accessibilitySkill
- flutter-cachingSkill
- flutter-clean-archSkill
- flutter-concurrencySkill
- flutter-design-systemSkill
- flutter-diSkill
- flutter-formsSkill
- flutter-monorepoSkill
- flutter-new-moduleSkill
- flutter-runSkill
- flutter-stateSkill
- flutter-store-cdSkill
- flutter-testingSkill
- flutter-themingSkill
- flutter-webSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 2 are distinctive, and 6 are 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.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 22
Never spend a review comment on something dart format or dart analyze will fix.
- Very specific: line 72
!is a claim you are making to the compiler: Every one is a potential runtime crash. - Very specific: line 92
Prefer late final over a nullable field you know gets set once in initState.
- Very specific: line 129
Avoid default: in a switch over a sealed type.
- Very specific: line 157
If you override ==, override hashCode, and don't define == on a mutable class — a mutated key silently disappears from any Set or Map …
- Distinctive: line 257
Never log a credential, token, or personally identifying field.
- Very specific: line 262
Use package:checks for unit and service assertions — better failure messages and type-safe chaining than bare expect.
- Distinctive: line 302
Add rules deliberately. A rule nobody agreed to becomes a rule everybody suppresses.
≈ “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
You are writing or reviewing Dart for craft — the layer below architecture. Where a project convention exists, it wins; this is what to do when there isn't one, and what the analyzer will not tell you.
Related in this repo: flutter-clean-arch for layering, flutter-testing for what to test, flutter-concurrency for isolates and streams, flutter-accessibility for semantics, flutter-theming for colour and type.
Some topics below have a canonical skill from the Dart team (dart-lang/skills) that goes deeper than this file should. Where one exists it is named inline — install it and defer to it. What stays here is the opinion: which convention this codebase picked, and the traps the analyzer won't flag.
The first rule: let the tools own what the tools own
Never spend a review comment on something dart format or dart analyze will
fix. Formatting, unused imports, missing const where the lint catches it —
all machine work.
dart format . # not `flutter format`, removed in Flutter 3.38
dart fix --apply # applies analyzer-suggested fixes
dart analyzeSetting up and driving the analyzer is
dart-run-static-analysis's job.
Review semantics: a name that misleads, an error swallowed, a shape that won't survive the next change.
Naming
| Thing | Case | Note |
| Classes, enums, typedefs, extensions | UpperCamelCase | |
| Members, variables, parameters, constants | lowerCamelCase | constants too — not SCREAMING_CAPS |
| Files, directories, import prefixes | lowercase_with_underscores | |
| Private | leading _ |
Beyond case:
- Put the most descriptive noun last.
pageCount, notnumPages.inputUserName, notuserNameInput. The last word is what a reader scanning a column of names actually sees. - Don't abbreviate unless the abbreviation is more common than the word
(
id,http,dbare fine;usr,btn,cfgare not). Abbreviation drift inside one file —userin one method andusrin the next — is worth a review comment even though it changes no behaviour. - Non-boolean property → noun phrase (
elements,remainingBudget). Function that does something → verb phrase (sortItems(),sendInvite()). Boolean → non-imperative predicate that reads as an assertion:isEmpty,hasElements,canClose— nevercloseorgetIsEmpty. - Don't prefix getters with
get. In Dart the access syntax already says it:user.name, notuser.getName(). toX()returns a new object;asX()returns a view or wrapper over the same data. Picking the wrong one tells the caller the wrong thing about cost and about whether mutations alias.- Don't repeat the parameter's type in the name.
addPerson(Person p), notaddPersonWithPerson. The signature is right there.
Null safety
Soundly null-safe code is the default; the interesting part is what to do at the boundary.
!is a claim you are making to the compiler. Every one is a potential runtime crash. Use it only where the invariant is genuinely local and obvious, and prefer restructuring so the compiler can see it:// avoid — the ! is load-bearing and far from the check if (user != null) { ... } doSomething(user!.name); // prefer — promote once, into a non-nullable local final u = user; if (u != null) doSomething(u.name);A field can't be promoted (another isolate or subclass could change it), which is why the local is necessary.
- Reach for the null-aware operators —
?.,??,??=,...?— before an explicitif (x == null). - Don't initialise to
nullexplicitly.int? count;is already null. - Prefer
late finalover a nullable field you know gets set once ininitState. It fails loudly at the wrong access instead of spreading?through callers. - At an API boundary you don't control (JSON, platform channels), validate once
on the way in and hand the rest of the codebase non-nullable types. Don't let
Map<String, dynamic>nullability leak past the model layer.
Pattern matching over a sealed status
The general feature is
dart-use-pattern-matching's subject. What follows is only how it pairs with the sealed status classes this codebase uses.
Prefer
switchexpressions over statements when producing a value. They are exhaustive over a sealed hierarchy, so adding a variant becomes a compile error instead of a silently missed branch — which is the whole reason to seal a status class in the first place (see flutter-state).final label = switch (status) { LoadingStatus() => 'Loading…', SuccessStatus(:final items) => '${items.length} found', ErrorStatus(:final message) => message, };:final itemsdestructures in the pattern — no cast, no second lookup.Records for a genuinely anonymous pair, when a named class would be ceremony and the tuple never leaves the file:
(int width, int height) measure() => (800, 600); final (w, h) = measure();Once a record crosses a layer boundary or gains a third field, make it a class. Records have no name, so they can't carry meaning or invariants.
- Avoid
default:in a switch over a sealed type. It defeats exhaustiveness and turns the next added variant into a runtime bug.
Error handling
catchwithout anonclause catches everything, including programming errors you wanted to crash on. Catch the type you can act on.- Never swallow. An empty
catch {}, or one that only returnsnull, destroys the stack trace at the exact moment you needed it. rethrow, notthrow e—throw erestarts the stack trace at the catch site and you lose the origin.- Preserve the stack trace when you cross a layer:
catch (e, s)and passsalong. A domain exception with no trace is a bug report you can't act on. - Throw objects implementing
Exception(recoverable, expected) orError(a bug — don't catch these). Never throw a rawString.
Members and constructors
- Prefer
finalfields. Immutability removes a whole class of question from every read. - Don't wrap a field in a trivial getter/setter. Dart lets you turn a field into a getter later without changing call sites — that's the point of uniform access. Add the getter when it does something.
- Use
=>for members that are a single expression, and a block body when it isn't. Don't chain=>across three lines to avoid braces. - Initialising formals:
Foo(this.bar)over an assignment in the body. constconstructors wherever the class allows it — this is what lets callers build the widget once and skip rebuilds.- If you override
==, overridehashCode, and don't define==on a mutable class — a mutated key silently disappears from anySetorMapholding it. For value objects,Equatableor a record is usually less risk than hand-writing both.
Type annotations
Annotate where it's a contract or where inference can't see it; stay quiet where the compiler already knows:
- Do annotate: return types, parameter types, fields and top-level variables without an obvious initialiser.
- Don't annotate: locals with an initialiser, and lambda parameters whose type comes from context.
// the types add nothing here
ListView.builder(
itemBuilder: (context, index) => ItemCard(items[index]),
);
// but here the annotation is the only thing documenting the shape
final Map<String, List<int>> byCategory = {};An empty collection literal with no annotation infers dynamic and quietly
disables type checking for everything downstream.
Widget-level quality
- A private widget class beats a
Widget _buildHeader()helper. It isn't style: a helper method has no element of its own, so it rebuilds whenever the parent does and can never beconst. Aclass _Header extends StatelessWidgetgets its own element and can be skipped entirely. - Break up a long
build()the same way — into widget classes, not more helpers. - No expensive work in
build(). It runs on every frame that touches this subtree. Network calls, sorting a large list, parsing — all belong above it. Heavy pure computation goes tocompute()/Isolate.run(); see flutter-concurrency. - Dispose what you create: controllers,
StreamSubscriptions,Tickers,FocusNodes. If a field has adispose(), yourStateneeds one. BuildContextacross anawaitis a bug waiting to happen. Guard it:await repository.save(item); if (!context.mounted) return; Navigator.of(context).pop();use_build_context_synchronouslycatches the common shape, not all of them.- Stable
Keys on dynamic lists. Without one, Flutter matches children by position, so an insert or reorder keeps the wrong state attached to the wrong item — the classic "the checkbox moved to another row" bug. ListView.builder, never aListView(children: [...])for a list whose length you don't control. The non-builder form materialises every child.
Documentation
Formatting rules — summary sentence, third-person verbs,
[brackets], where the comment sits relative to annotations — aredart-write-documentation's subject, and it is the authority. Two judgement calls it can't make for you:
Document what a caller can't see, not what the signature already shows. Nullability, what it throws and when, side effects, cost. A doc comment that re-lists the parameters has added nothing.
/// Fetches the profile for [userId]. /// /// Returns `null` when no such user exists. Throws [NetworkException] if the /// request fails for connectivity reasons — callers that can retry should /// catch it; other failures are programming errors and propagate. Future<User?> fetchUser(String userId) async { ... }- Comment why, not what.
// increment iis noise.// the API is 1-basedis the reason the next person doesn't "fix" it back into a bug.
Logging
print() is for scratch debugging and the avoid_print lint exists for a
reason. Use dart:developer, which DevTools understands:
import 'dart:developer' as developer;
try {
await repository.sync();
} catch (e, s) {
developer.log(
'Sync failed',
name: 'myapp.sync', // shows as a filterable channel in DevTools
level: 1000, // 1000 = SEVERE, per package:logging levels
error: e,
stackTrace: s,
);
}Never log a credential, token, or personally identifying field. In an app that handles user data, assume every log line reaches a crash reporter.
Assertion style in tests
Use package:checks for unit and service assertions — better failure messages
and type-safe chaining than bare expect. dart-migrate-to-checks-package
covers the migration and the full matcher mapping; the two things worth fixing
as convention here:
check(user)
..has((u) => u.name, 'name').equals('Ana')
..has((u) => u.email, 'email').contains('@');- Prefer
.has((u) => u.field, 'field')over comparing whole objects. It makes the failure name which field differed instead of dumping two objects side by side and leaving you to diff them. - Widget tests keep
expectwithfindsOneWidget/findsNothing— those areflutter_testmatchers with nochecksequivalent. Both styles in one file is fine; they solve different problems.
See flutter-testing for structure, mocking and what deserves a test at all.
Lint setup
dart-run-static-analysis owns running the analyzer and dart fix. The
convention worth stating: promote a lint to error when violating it ships a
bug; leave it a warning when it is taste — and always exclude generated
files, because noise you can't fix trains people to ignore the analyzer.
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
use_build_context_synchronously: error # real crashes, not style
unawaited_futures: error
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"Add rules deliberately. A rule nobody agreed to becomes a rule everybody suppresses.
Review checklist
- Would
dart format/dart analyzehave caught it? Then don't raise it. - Does any name mislead, or drift from the name used three lines up?
- Is there a
!whose invariant isn't visible within a few lines? - Is any error swallowed, or rethrown as
throw elosing the trace? - Is a
switchover a sealed type usingdefault:? - Does a
Statecreate something with adispose()and not dispose it? - Is
contextused after anawaitwithout amountedguard? - Is a
Widget _buildX()helper doing what a private widget class should? - Does a new public API have a doc comment saying what a caller can't see?
- Is anything logged that shouldn't leave the device?
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.

