Skip to content
Back to skills

Flutter Dart Style

ASecurity

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.

  • 11 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
developmentgobashnodeexpresstestingdebugginggitapidocumentation

Works with

  • api

Security analysis

A100/100

Scanned September 19, 2026

npx -y skills add gabuldev/flutter-skills --skill flutter-dart-style --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flutter Dart Style?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flutter Dart Style
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gabuldev-flutter-dart-style/badge)](https://www.skillsdirectory.com/skills/gabuldev-flutter-dart-style)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flutter-dart-style
description: 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.
---

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](https://github.com/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.

```bash
dart format .        # not `flutter format`, removed in Flutter 3.38
dart fix --apply     # applies analyzer-suggested fixes
dart analyze
```

> Setting 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`, not `numPages`.
  `inputUserName`, not `userNameInput`. 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`, `db` are fine; `usr`, `btn`, `cfg` are not). Abbreviation drift
  inside one file — `user` in one method and `usr` in 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` — never `close` or `getIsEmpty`.
- **Don't prefix getters with `get`.** In Dart the access syntax already says
  it: `user.name`, not `user.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)`, not
  `addPersonWithPerson`. 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:

  ```dart
  // 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
  explicit `if (x == null)`.
- **Don't initialise to `null` explicitly.** `int? count;` is already null.
- Prefer `late final` over a nullable field you know gets set once in `initState`.
  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 `switch` expressions** 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**).

  ```dart
  final label = switch (status) {
    LoadingStatus()      => 'Loading…',
    SuccessStatus(:final items) => '${items.length} found',
    ErrorStatus(:final message) => message,
  };
  ```

  `:final items` destructures 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:

  ```dart
  (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

- **`catch` without an `on` clause 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 returns `null`,
  destroys the stack trace at the exact moment you needed it.
- **`rethrow`, not `throw e`** — `throw e` restarts 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 pass `s`
  along. A domain exception with no trace is a bug report you can't act on.
- Throw objects implementing `Exception` (recoverable, expected) or `Error`
  (a bug — don't catch these). Never throw a raw `String`.

## Members and constructors

- **Prefer `final` fields.** 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.
- **`const` constructors wherever the class allows it** — this is what lets
  callers build the widget once and skip rebuilds.
- If you override `==`, **override `hashCode`**, and don't define `==` on a
  mutable class — a mutated key silently disappears from any `Set` or `Map`
  holding it. For value objects, `Equatable` or 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.

```dart
// 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 be `const`. A `class _Header extends StatelessWidget`
  gets 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 to `compute()` / `Isolate.run()`; see
  **flutter-concurrency**.
- **Dispose what you create**: controllers, `StreamSubscription`s, `Ticker`s,
  `FocusNode`s. If a field has a `dispose()`, your `State` needs one.
- **`BuildContext` across an `await` is a bug waiting to happen.** Guard it:

  ```dart
  await repository.save(item);
  if (!context.mounted) return;
  Navigator.of(context).pop();
  ```

  `use_build_context_synchronously` catches the common shape, not all of them.
- **Stable `Key`s 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 a `ListView(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 — are `dart-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.

  ```dart
  /// 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 i` is noise. `// the API is 1-based`
  is 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:

```dart
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:

```dart
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 `expect`** with `findsOneWidget` / `findsNothing` — those
  are `flutter_test` matchers with no `checks` equivalent. 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.

```yaml
# 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

1. Would `dart format` / `dart analyze` have caught it? Then don't raise it.
2. Does any name mislead, or drift from the name used three lines up?
3. Is there a `!` whose invariant isn't visible within a few lines?
4. Is any error swallowed, or rethrown as `throw e` losing the trace?
5. Is a `switch` over a sealed type using `default:`?
6. Does a `State` create something with a `dispose()` and not dispose it?
7. Is `context` used after an `await` without a `mounted` guard?
8. Is a `Widget _buildX()` helper doing what a private widget class should?
9. Does a new public API have a doc comment saying what a caller can't see?
10. Is anything logged that shouldn't leave the device?

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.