A Flutter application built on a clean feature-first + layered architecture, modeled after DPIP.
mise is required. Not preferred — required. Flutter and Dart are pinned in
mise.toml, and every workflow has a script under tool/. Never type the
toolchain yourself — not flutter, not dart, and not mise exec. A shell's
PATH is resolved once and mise activate caches it, so a toolchain bump leaves
the old SDK on PATH until the session is replaced, and a run against the wrong
SDK announces nothing: it builds, it runs, its tests pass.
| Do this | Run |
|---|---|
| One-time setup (git hooks + build info) | tool/dev/setup.sh |
| Start the app | tool/run.sh -d "<device>" |
| Run the tests | tool/dev/test.sh |
| Format check + analyzer | tool/dev/analyze.sh |
| Reformat in place | tool/dev/format.sh |
| Resolve dependencies | tool/dev/deps.sh |
| Everything CI runs | tool/check.sh |
| Before writing any commit | tool/commit.sh |
tool/run.sh installs the git hooks itself on first launch. On Windows use
tool\run.ps1 (uncoloured log), or bash tool/run.sh under Git Bash / WSL.
lib/
├── main.dart # entry point → bootstrap()
├── bootstrap.dart # init platform services then runApp
├── app/ # app shell: wiring, not features
│ ├── app.dart # root MaterialApp.router + localization
│ ├── router/ # go_router route table
│ └── theme/ # Material 3 tokens
├── core/ # cross-cutting, feature-agnostic building blocks
│ ├── error/ # Result + Failure hierarchy
│ └── logging/ # the Log facade — never print/debugPrint
├── features/ # one folder per feature, each self-contained
│ └── <feature>/
│ ├── data/ # datasources, repository impls
│ ├── domain/ # entities, repository interfaces (pure Dart)
│ └── presentation/ # pages/, widgets/, controllers
├── l10n/ # ARB sources; generated code in l10n/gen
└── shared/ # reused across ≥2 features
└── navigation/ # AppRoutes (route names)
Layer rules:
- presentation depends on domain; never on data directly.
- domain is pure Dart (no Flutter, no Dio).
- core and shared must not import from features.
- Features must not import each other's internals; share via
shared/. - Navigate by name through
AppRoutes; the router is the only place that imports a page widget.