---
name: flutter-best-practices
description: "Use when writing, reviewing, refactoring, or planning Flutter/Dart code — screens, features, project structure, state management, folders, widgets, cubits/blocs, repositories, services, or tests."
---

# Flutter Best Practices

Standards for building maintainable Flutter apps, distilled from the official
Flutter architecture guide and LeanCode's experience shipping 40+ Flutter
projects (including a 30-developer banking app). Apply these when writing new
code; when touching existing code, prefer consistency with the surrounding
codebase and raise conflicts with these standards rather than silently
rewriting.

## How to use this skill

Read the reference file that matches the task. Read more than one when tasks
overlap (e.g. a new feature touches both structure and coding style).

| Task | Read |
|---|---|
| Design layers, decide where logic lives, MVVM, repositories/services | [references/architecture-layers.md](references/architecture-layers.md) |
| Create/organize a feature, folder structure, state management wiring | [references/feature-structure.md](references/feature-structure.md) |
| Write or review Dart/Flutter code, widgets, tests, lints | [references/dart-coding-practices.md](references/dart-coding-practices.md) |
| Multi-team/multi-package apps, monorepo, navigation, localization, API contracts, E2E tests | [references/enterprise-scale.md](references/enterprise-scale.md) |
| App localization setup, reusable UI package string ownership, language picker visibility | [references/localization-package-boundaries.md](references/localization-package-boundaries.md) |

For a quick task (small widget fix, one-line review comment), the core rules
below may be enough on their own.

## Core rules (always apply)

### Architecture

1. **Separate UI from data.** Two broad layers: UI (views + view models /
   cubits) and Data (repositories + services). Dependencies point one way:
   `View → ViewModel → Repository → Service`. Lower layers never import upper
   layers. Repositories never depend on each other.
2. **Views hold no business logic.** Widgets may contain show/hide
   conditionals, animation, layout, and simple routing logic — nothing that
   transforms or decides about data. All data logic lives in the view model
   (or cubit/bloc), which has no access to `BuildContext`.
3. **Organize by feature, not by layer.** Everything a feature needs — state
   management, widgets, models — lives under one feature directory. Don't
   create top-level `blocs/`, `widgets/`, `models/` buckets that scatter a
   feature across the tree.
4. **State is immutable and explicit.** Model UI state as a sealed/union type
   (initial / inProgress / failure / ready) so every case is handled
   exhaustively. One-off effects (snackbars, navigation) are events, not
   state.
5. **Add layers only when they pay for themselves.** Start with
   view-model → API client. Introduce a repository when you need caching,
   offline, or merging sources. Introduce a use case only when logic merges
   multiple repositories, is genuinely complex, or is reused by several view
   models.

### Coding

6. **Prefer intent-revealing widgets over `Container`.** Use `Padding`,
   `SizedBox`, `ColoredBox`, `DecoratedBox`, `Center` — they are const-able
   and self-describing. `Container` is fine only when combining several
   properties at once.
7. **Use modern Dart.** Pattern matching (`if (x case final v?)`), switch
   expressions with exhaustiveness, records and destructuring, collection
   `if`/`for`/spreads instead of `.add()` loops, expression bodies for
   pass-through async functions (no redundant `async`/`await`).
8. **Prefix sliver-returning widgets with `Sliver`** so misuse in the wrong
   scroll context is caught at a glance.
9. **Tests tell a story.** Use expressive matchers (`isEmpty`, `throwsA`,
   `isA`, `completion`) and minimize dependencies — plain `Text`/`SizedBox`
   over design-system widgets in test fixtures. Test cubits/view models in
   isolation from the widget tree.
10. **Every `// ignore:` gets a reason** on the same or preceding line. Log
    errors with dedicated `error`/`stackTrace` parameters, never string
    interpolation.

## Workflow checklists

### Adding a new feature/screen

1. Read [references/feature-structure.md](references/feature-structure.md)
   and mirror the existing project's conventions for the feature directory.
2. Define the state as a union type first; then the cubit/view model; then
   the widgets. Constructor-inject dependencies; scope them to the feature's
   widget subtree.
3. Data comes in through a repository or typed API client — never fetched
   inside a widget.
4. Add unit tests for the cubit/view model logic before wiring UI details.

### Reviewing Flutter code

Check, in order of importance:

1. Logic in the right layer (rule 1–2)? Any `BuildContext` in business logic?
2. State modeled as immutable union types, all cases handled?
3. Feature self-contained, or does it reach into another feature's internals?
4. Widget choices (rule 6), modern Dart (rule 7), sliver naming (rule 8)?
5. Tests present for logic, readable, minimal dependencies?
6. Unexplained `// ignore:`, string-interpolated error logs, deprecated
   patterns still spreading?

### Starting a new project

1. Read [references/architecture-layers.md](references/architecture-layers.md)
   for the layer blueprint and
   [references/feature-structure.md](references/feature-structure.md) for the
   folder skeleton.
2. If more than ~2 teams or clearly separable domains are involved, read
   [references/enterprise-scale.md](references/enterprise-scale.md) and
   consider a Melos monorepo with one package per domain from day one.
3. Set up strict lints early (`leancode_lint` or equivalent + custom rules) —
   retrofitting is far more expensive.

## Package palette

Defaults that these standards assume (swap for project-local equivalents when
the codebase already uses something else):

- **State:** `bloc` (Cubit) + `freezed` for union-type states;
  `bloc_presentation` for one-off UI events
- **DI:** `provider` scoped to widget subtrees (accepting its lack of
  compile-time safety as the lesser evil)
- **Boilerplate reduction:** `flutter_hooks`
- **Monorepo:** `melos`
- **Localization:** `flutter_localizations` + `intl` with `.arb` files
- **Lints:** `leancode_lint`
- **E2E/UI tests:** `patrol`

The official Flutter guide is state-management-agnostic (MVVM with
ChangeNotifier works too); what matters is the layer separation, not the
package. See the reference files for rationale and trade-offs.
