App Architecture Ladder
Use the smallest structure that still makes change safe. Flutter’s official app architecture guidance is useful, but applying the full shape to a tiny app creates ceremony before it creates value.
Ladder
| Stage | Use when | Shape |
|---|---|---|
| 1. Single screen | Prototype, demo, throwaway UI | main.dart, a few widgets, local state |
| 2. Feature folders | More than one screen or flow | features/<feature>/, reusable widgets, route names |
| 3. UI plus data | App reads/writes storage or network | Views call view models, view models call repositories |
| 4. MVVM per feature | Screens have non-trivial state and commands | View plus view model per feature, immutable UI state |
| 5. Domain layer | Business rules span repositories or use cases | Use cases/interactors between view models and repositories |
| 6. Multi-package app | Multiple teams, shared modules, plugin packages | Package boundaries, published APIs, stricter CI |
Recommended Solo-Dev Default
Start at stage 2 or 3 for real apps:
lib/
main.dart
app.dart
routing.dart
features/
expenses/
expenses_view.dart
expenses_view_model.dart
expenses_repository.dart
services/
local_database.dart
platform_notifications.dart
test/
features/
expenses/
expenses_view_model_test.dart
expenses_view_test.dart
This is enough to keep widgets light without inventing packages, modules, or an abstract domain layer before the app earns them.
Rules
| Rule | Why |
|---|---|
| Keep business logic out of widgets | Widget tests stay focused and logic gets unit tests |
| Let view models expose UI state and commands | Views remain declarative and easy to rebuild |
| Put persistence/network/platform access behind repositories or services | Tests can fake dependencies and platform changes stay localized |
| Add domain/use-case classes only for repeated business rules | Avoid a folder full of pass-through wrappers |
| Wire dependencies manually first | Dependency injection packages are useful only after object graphs hurt |
Gotcha: A
features/,domain/,data/,core/, andshared/tree does not make an app maintainable by itself. The dependencies and tests matter more than folder names.