Flutter MVVM Architecture
This skill encodes the Flutter team's official recommended app architecture:
https://docs.flutter.dev/app-architecture, including the
core architecture concepts
(separation of concerns, single source of truth, unidirectional data flow,
immutable state) summarized under "Core principles" below. It's the same
architecture used in Flutter's reference sample, the
Compass app.
Apply it by default whenever generating or reviewing non-trivial Flutter code
(more than a single throwaway widget). These are the Flutter team's
recommendations, not rigid law — treat them as strong defaults, and adapt
when a repo already has an established, different convention (e.g. it's
already fully built on riverpod or flutter_bloc). Even then, keep the
underlying principles: separation of concerns, single source of truth,
unidirectional data flow, and dumb widgets.
Verified against the live docs on 2026-08-14 (site showed Flutter 3.44.7,
pages last updated May–July 2026). If provider, go_router, or freezed
ship a breaking major version, or docs.flutter.dev/app-architecture shows a
materially newer revision, re-check that page before trusting the
package-specific code samples in this skill.
Why this matters
Good architecture makes Flutter apps easier to maintain, test, and scale
across a growing team, and it's what most production Flutter codebases
converge on. Code that mixes network calls, business logic, and widget code
in one StatefulWidget works for a demo but rapidly becomes unmaintainable
and untestable. Defaulting to this layered structure avoids that outcome
without meaningfully slowing down simple requests.
Core principles (always apply, regardless of feature size)
- Separation of concerns. Split the app into a UI layer and a
data layer (plus an optional domain layer). Layers may only talk to
the layer directly above or below them — the UI layer never touches the
data layer directly, and vice versa.
- Single source of truth (SSOT). Every type of data has exactly one
owner: a Repository. If data can change, the repository is the only
class allowed to change it.
- Unidirectional data flow (UDF). User events flow UI → logic → data.
New state flows data → logic → UI. Data never mutates in place inside a
widget or view model.
- UI is a function of (immutable) state. Widgets render, they don't
decide. Data should be immutable; when it changes, a new instance is
created and the UI rebuilds in response.
- Widgets stay dumb. A widget/View should hold as little logic as
possible — see the exact allowed list under Views below.
The four core components
MVVM here maps View + ViewModel to the UI layer, and Repository +
Service to the Model layer of classic MVVM.
| Component |
Layer |
Responsibility |
Depends on |
| View |
UI |
A widget or composition of widgets for one feature (often, not always, a full screen with a Scaffold). Renders data, forwards user events. No business logic. |
Exactly one ViewModel |
| ViewModel |
UI |
Converts domain data into UI state; exposes Commands (callbacks) that Views attach to event handlers; calls notifyListeners() when state changes. |
One or more Repositories (and/or Use-cases) |
| Use-case (optional, domain layer) |
Domain |
Encapsulates business logic that's reused across ViewModels or that merges data from multiple repositories. Only add when needed — see references/recommendations.md. |
One or more Repositories |
| Repository |
Data |
The single source of truth for one type of application data. Polls/refreshes services, caches, retries, and transforms raw data into domain models. |
One or more Services |
| Service |
Data |
A stateless wrapper around exactly one external data source (REST API, platform plugin, local database, local files). Holds no state, has no business logic. |
Nothing else in the app |
A View and ViewModel have a strict one-to-one relationship and together
define one "feature" (not always a full screen — e.g. a reusable
LogoutButton/LogoutViewModel pair). Repositories and ViewModels have a
many-to-many relationship, as do Services and Repositories.
When to add a domain layer
Add a Use-case only when at least one of these is true — otherwise let
ViewModels call Repositories directly, which is the default:
- The logic merges data from multiple repositories.
- The same logic is reused by more than one ViewModel.
- The logic is exceedingly complex and crowds the ViewModel.
Default to skipping it. A single-repository CRUD screen with no shared logic
doesn't need a use-case just to feel "more layered" — that's added
boilerplate and mocking overhead with no testability or reuse benefit.
For full code walkthroughs of each component, read:
references/ui-layer.md — Views, ViewModels, UI state, the Command pattern
references/data-layer.md — Repositories, Services, domain models, Result
Rules of engagement (who may talk to whom)
| Component |
Allowed to know about |
Never allowed to know about |
| View |
Exactly one ViewModel (passed into its constructor) |
Any repository, service, or other view model |
| ViewModel |
One or more Repositories / Use-cases (passed into its constructor, stored as private fields) |
That a View exists, other ViewModels |
| Repository |
One or more Services (passed into its constructor, stored as private fields) |
Other repositories, any ViewModel |
| Service |
Nothing else in the app |
Repositories, ViewModels, Views |
Always inject dependencies through constructors (required this.xRepository)
and store them as private (_-prefixed) fields — this is what stops a View
from reaching past its ViewModel into the data layer. See
references/dependency-injection.md for the full wiring pattern
(the Flutter team recommends package:provider; get_it/injectable
service-locator-style DI is a valid alternative if a repo already uses it —
the constructor-injection and privacy rules above still apply either way).
Workflow for building or reviewing a feature
- Identify the feature — it's exactly one View + one ViewModel pair.
Name them
<Feature>Screen/<Feature>View and <Feature>ViewModel.
- Identify the data types the feature needs. Each distinct type of
data gets (or reuses) its own
Repository (e.g. UserRepository,
BookingRepository). Don't let one repository own multiple unrelated
data types, and don't let repositories know about each other — if two
repositories' data needs to be combined, do that in the ViewModel or a
use-case, not the repository.
- Identify the external sources the data comes from. Each REST API,
platform plugin, or local database gets its own
Service
(e.g. ApiClient, SharedPreferencesService). A repository composes
one or more services.
- Write the ViewModel. Constructor takes repositories/use-cases as
required named parameters; expose UI state via public getters backed by
private fields; expose actions as
Command objects (see
references/design-patterns.md); extend ChangeNotifier and call
notifyListeners() whenever state changes.
- Write the View. A widget whose only inputs are
key and the
ViewModel; wrap the parts that need to rebuild in ListenableBuilder
(listening to the ViewModel or, for finer control, to an individual
Command); forward taps/gestures straight to viewModel.someCommand.execute(...).
- Wire dependency injection at the app/router level — don't
instantiate repositories or services inside widgets or view models.
- Consider error handling and loading state up front using
Result
and Command (see references/design-patterns.md) rather than bolting
them on later — almost every real ViewModel action needs a
running/error/success representation.
- Write tests alongside the code: unit tests for the ViewModel against
a fake repository, unit tests for the repository against a fake service,
and widget tests for the View. See
references/testing.md.
Migrating existing code? If you're converting a StatefulWidget/
setState() screen rather than starting fresh, follow
references/migration.md instead of starting at step 1 above — it walks the
same end state through incremental, compilable steps.
Auditing existing code for separation-of-concerns problems? Scan it
against references/audit-checklist.md rather than rebuilding the workflow
above from scratch.
Recommended project structure
lib/
ui/
core/ # shared widgets & themes used by multiple features
ui/
themes/
<feature_name>/
view_models/
<feature>_view_model.dart
widgets/
<feature>_screen.dart
<other widgets specific to this feature>
domain/
models/
<model_name>.dart # domain models, shared by data + ui layers
data/
repositories/
<repository_name>.dart
services/
<service_name>.dart
model/
<api_model_name>.dart # raw API/DB models, distinct from domain models
config/
utils/
routing/
main_development.dart
main_staging.dart
main.dart
test/ # mirrors lib/ — unit + widget tests
testing/ # fakes and mocks shared across test files
fakes/
models/
Notes on this structure (from the Flutter team, based on the Compass app):
data/ is organized by type (repositories, services) because those
classes are reused across features.
ui/ is organized by feature because each feature owns exactly one
View + ViewModel pair.
domain/models holds the shared domain model classes, since both the
data and UI layers depend on them.
- Multiple
main_*.dart entry points support different environments
(development/staging/production), typically wiring up different
repository implementations (e.g. BookingRepositoryRemote vs
BookingRepositoryLocal).
Non-negotiable rules when generating Flutter code
Apply these unless the user has explicitly asked for a different pattern or
the surrounding codebase clearly already follows one:
- Never put business logic, network calls, or data mutation inside a
widget's
build() method or event handlers. Only these are allowed
directly in a View: simple if/else to show/hide widgets based on a flag
from the ViewModel, animation logic, layout logic based on device info
(screen size/orientation), and simple routing.
- Never let a widget hold a
Repository or Service reference.
Widgets only ever see a ViewModel.
- Always make domain/UI-state data models immutable. Prefer
freezed
(or built_value) for data classes so copyWith/equality/serialization
are generated instead of hand-rolled.
- Always expose ViewModel actions as methods that can be attached to
event handlers — prefer the
Command pattern (see
references/design-patterns.md) over ad-hoc bool isLoading /
String? error fields duplicated per action.
- Always make repositories, not services, the thing ViewModels depend
on. A ViewModel should never import an API client or a database
service directly.
- Always use dependency injection (constructor injection, wired via
provider or an equivalent mechanism like get_it at the widget-tree or
router level) instead of singletons, global variables, or ad-hoc statics
reached for from inside business logic.
- Prefer
Result<T>-returning methods over throwing exceptions across
service/repository/view-model boundaries so callers can't forget to
catch (see references/design-patterns.md).
- Write tests that exploit the architecture: fake the repository to
test a ViewModel, fake the service to test a repository. If a class is
hard to fake or mock, that's a signal the class has too many
responsibilities or unclear inputs/outputs.
Reference files — read these for implementation detail
| File |
Read when you need... |
references/ui-layer.md |
Full View/ViewModel code patterns, UI state, ListenableBuilder wiring, the Command pattern in context, and disposing ViewModels/Commands/subscriptions |
references/data-layer.md |
Full Repository/Service code patterns, domain vs. API models, the Result pattern in context |
references/dependency-injection.md |
Wiring everything together with provider + go_router, the full rules-of-engagement table |
references/migration.md |
Converting a StatefulWidget/setState() screen into View + ViewModel + Repository, step by step |
references/testing.md |
Unit-testing ViewModels/Repositories with fakes, widget-testing Views |
references/audit-checklist.md |
A quick-scan list of architecture smells for reviewing existing code against this skill's rules |
references/design-patterns.md |
Full Command/Result class implementations, Optimistic State, key-value persistence, SQL persistence, offline-first strategies |
references/recommendations.md |
The Flutter team's full prioritized do/don't tables, naming conventions, and recommended packages/resources |
Load only the reference files relevant to the current task — don't read all
of them for a small, single-widget request.
1---2name: flutter-mvvm-architecture3description: Apply the Flutter team's official app-architecture guidelines (docs.flutter.dev/app-architecture, the MVVM pattern used by the Compass sample app) whenever writing, reviewing, refactoring, or reasoning about Flutter/Dart code. Trigger this for anything touching Flutter widgets, screens, ViewModels, state management, repositories, services, dependency injection, Provider, ChangeNotifier, Command objects, Result/error-handling types, or Flutter project/folder structure — even if the user doesn't say "architecture" or "MVVM" explicitly. Also trigger when scaffolding a new Flutter feature or screen, deciding where business logic or a network call should live, converting ad-hoc setState()/StatefulWidget logic into a layered design, adding persistence (SharedPreferences/SQL) or offline support, or auditing existing Flutter code for separation-of-concerns problems.4license: MIT5---67# Flutter MVVM Architecture89This skill encodes the Flutter team's official recommended app architecture:10<https://docs.flutter.dev/app-architecture>, including the11[core architecture concepts](https://docs.flutter.dev/app-architecture/concepts)12(separation of concerns, single source of truth, unidirectional data flow,13immutable state) summarized under "Core principles" below. It's the same14architecture used in Flutter's reference sample, the15[Compass app](https://github.com/flutter/samples/tree/main/compass_app).16Apply it by default whenever generating or reviewing non-trivial Flutter code17(more than a single throwaway widget). These are the Flutter team's18recommendations, not rigid law — treat them as strong defaults, and adapt19when a repo already has an established, different convention (e.g. it's20already fully built on `riverpod` or `flutter_bloc`). Even then, keep the21underlying principles: separation of concerns, single source of truth,22unidirectional data flow, and dumb widgets.2324> Verified against the live docs on 2026-08-14 (site showed Flutter 3.44.7,25> pages last updated May–July 2026). If `provider`, `go_router`, or `freezed`26> ship a breaking major version, or docs.flutter.dev/app-architecture shows a27> materially newer revision, re-check that page before trusting the28> package-specific code samples in this skill.2930## Why this matters3132Good architecture makes Flutter apps easier to maintain, test, and scale33across a growing team, and it's what most production Flutter codebases34converge on. Code that mixes network calls, business logic, and widget code35in one `StatefulWidget` works for a demo but rapidly becomes unmaintainable36and untestable. Defaulting to this layered structure avoids that outcome37without meaningfully slowing down simple requests.3839## Core principles (always apply, regardless of feature size)4041- **Separation of concerns.** Split the app into a **UI layer** and a42 **data layer** (plus an optional domain layer). Layers may only talk to43 the layer directly above or below them — the UI layer never touches the44 data layer directly, and vice versa.45- **Single source of truth (SSOT).** Every type of data has exactly one46 owner: a **Repository**. If data can change, the repository is the only47 class allowed to change it.48- **Unidirectional data flow (UDF).** User events flow UI → logic → data.49 New state flows data → logic → UI. Data never mutates in place inside a50 widget or view model.51- **UI is a function of (immutable) state.** Widgets render, they don't52 decide. Data should be immutable; when it changes, a new instance is53 created and the UI rebuilds in response.54- **Widgets stay dumb.** A widget/View should hold as little logic as55 possible — see the exact allowed list under Views below.5657## The four core components5859MVVM here maps `View` + `ViewModel` to the UI layer, and `Repository` +60`Service` to the Model layer of classic MVVM.6162| Component | Layer | Responsibility | Depends on |63|---|---|---|---|64| **View** | UI | A widget or composition of widgets for one feature (often, not always, a full screen with a `Scaffold`). Renders data, forwards user events. No business logic. | Exactly one ViewModel |65| **ViewModel** | UI | Converts domain data into UI state; exposes **Commands** (callbacks) that Views attach to event handlers; calls `notifyListeners()` when state changes. | One or more Repositories (and/or Use-cases) |66| **Use-case** *(optional, domain layer)* | Domain | Encapsulates business logic that's reused across ViewModels or that merges data from multiple repositories. Only add when needed — see `references/recommendations.md`. | One or more Repositories |67| **Repository** | Data | The single source of truth for one type of application data. Polls/refreshes services, caches, retries, and transforms raw data into **domain models**. | One or more Services |68| **Service** | Data | A stateless wrapper around exactly one external data source (REST API, platform plugin, local database, local files). Holds no state, has no business logic. | Nothing else in the app |6970A View and ViewModel have a strict **one-to-one** relationship and together71define one "feature" (not always a full screen — e.g. a reusable72`LogoutButton`/`LogoutViewModel` pair). Repositories and ViewModels have a73**many-to-many** relationship, as do Services and Repositories.7475### When to add a domain layer7677Add a Use-case only when at least one of these is true — otherwise let78ViewModels call Repositories directly, which is the default:79- The logic merges data from **multiple repositories**.80- The same logic is **reused by more than one ViewModel**.81- The logic is **exceedingly complex** and crowds the ViewModel.8283Default to skipping it. A single-repository CRUD screen with no shared logic84doesn't need a use-case just to feel "more layered" — that's added85boilerplate and mocking overhead with no testability or reuse benefit.8687For full code walkthroughs of each component, read:88- `references/ui-layer.md` — Views, ViewModels, UI state, the Command pattern89- `references/data-layer.md` — Repositories, Services, domain models, Result9091## Rules of engagement (who may talk to whom)9293| Component | Allowed to know about | Never allowed to know about |94|---|---|---|95| View | Exactly one ViewModel (passed into its constructor) | Any repository, service, or other view model |96| ViewModel | One or more Repositories / Use-cases (passed into its constructor, stored as **private** fields) | That a View exists, other ViewModels |97| Repository | One or more Services (passed into its constructor, stored as **private** fields) | Other repositories, any ViewModel |98| Service | Nothing else in the app | Repositories, ViewModels, Views |99100Always inject dependencies through constructors (`required this.xRepository`)101and store them as private (`_`-prefixed) fields — this is what stops a View102from reaching past its ViewModel into the data layer. See103`references/dependency-injection.md` for the full wiring pattern104(the Flutter team recommends `package:provider`; `get_it`/`injectable`105service-locator-style DI is a valid alternative if a repo already uses it —106the constructor-injection and privacy rules above still apply either way).107108## Workflow for building or reviewing a feature1091101. **Identify the feature** — it's exactly one View + one ViewModel pair.111 Name them `<Feature>Screen`/`<Feature>View` and `<Feature>ViewModel`.1122. **Identify the data types the feature needs.** Each distinct type of113 data gets (or reuses) its own `Repository` (e.g. `UserRepository`,114 `BookingRepository`). Don't let one repository own multiple unrelated115 data types, and don't let repositories know about each other — if two116 repositories' data needs to be combined, do that in the ViewModel or a117 use-case, not the repository.1183. **Identify the external sources the data comes from.** Each REST API,119 platform plugin, or local database gets its own `Service`120 (e.g. `ApiClient`, `SharedPreferencesService`). A repository composes121 one or more services.1224. **Write the ViewModel.** Constructor takes repositories/use-cases as123 required named parameters; expose UI state via public getters backed by124 private fields; expose actions as `Command` objects (see125 `references/design-patterns.md`); extend `ChangeNotifier` and call126 `notifyListeners()` whenever state changes.1275. **Write the View.** A widget whose only inputs are `key` and the128 ViewModel; wrap the parts that need to rebuild in `ListenableBuilder`129 (listening to the ViewModel or, for finer control, to an individual130 `Command`); forward taps/gestures straight to `viewModel.someCommand.execute(...)`.1316. **Wire dependency injection** at the app/router level — don't132 instantiate repositories or services inside widgets or view models.1337. **Consider error handling and loading state up front** using `Result`134 and `Command` (see `references/design-patterns.md`) rather than bolting135 them on later — almost every real ViewModel action needs a136 running/error/success representation.1378. **Write tests alongside the code**: unit tests for the ViewModel against138 a fake repository, unit tests for the repository against a fake service,139 and widget tests for the View. See `references/testing.md`.140141**Migrating existing code?** If you're converting a `StatefulWidget`/142`setState()` screen rather than starting fresh, follow143`references/migration.md` instead of starting at step 1 above — it walks the144same end state through incremental, compilable steps.145146**Auditing existing code for separation-of-concerns problems?** Scan it147against `references/audit-checklist.md` rather than rebuilding the workflow148above from scratch.149150## Recommended project structure151152```153lib/154 ui/155 core/ # shared widgets & themes used by multiple features156 ui/157 themes/158 <feature_name>/159 view_models/160 <feature>_view_model.dart161 widgets/162 <feature>_screen.dart163 <other widgets specific to this feature>164 domain/165 models/166 <model_name>.dart # domain models, shared by data + ui layers167 data/168 repositories/169 <repository_name>.dart170 services/171 <service_name>.dart172 model/173 <api_model_name>.dart # raw API/DB models, distinct from domain models174 config/175 utils/176 routing/177 main_development.dart178 main_staging.dart179 main.dart180181test/ # mirrors lib/ — unit + widget tests182testing/ # fakes and mocks shared across test files183 fakes/184 models/185```186187Notes on this structure (from the Flutter team, based on the Compass app):188- `data/` is organized **by type** (repositories, services) because those189 classes are reused across features.190- `ui/` is organized **by feature** because each feature owns exactly one191 View + ViewModel pair.192- `domain/models` holds the shared domain model classes, since both the193 data and UI layers depend on them.194- Multiple `main_*.dart` entry points support different environments195 (development/staging/production), typically wiring up different196 repository implementations (e.g. `BookingRepositoryRemote` vs197 `BookingRepositoryLocal`).198199## Non-negotiable rules when generating Flutter code200201Apply these unless the user has explicitly asked for a different pattern or202the surrounding codebase clearly already follows one:203204- **Never put business logic, network calls, or data mutation inside a205 widget's `build()` method or event handlers.** Only these are allowed206 directly in a View: simple if/else to show/hide widgets based on a flag207 from the ViewModel, animation logic, layout logic based on device info208 (screen size/orientation), and simple routing.209- **Never let a widget hold a `Repository` or `Service` reference.**210 Widgets only ever see a ViewModel.211- **Always make domain/UI-state data models immutable.** Prefer `freezed`212 (or `built_value`) for data classes so `copyWith`/equality/serialization213 are generated instead of hand-rolled.214- **Always expose ViewModel actions as methods that can be attached to215 event handlers** — prefer the `Command` pattern (see216 `references/design-patterns.md`) over ad-hoc `bool isLoading` /217 `String? error` fields duplicated per action.218- **Always make repositories, not services, the thing ViewModels depend219 on.** A ViewModel should never import an API client or a database220 service directly.221- **Always use dependency injection** (constructor injection, wired via222 `provider` or an equivalent mechanism like `get_it` at the widget-tree or223 router level) instead of singletons, global variables, or ad-hoc statics224 reached for from inside business logic.225- **Prefer `Result<T>`-returning methods over throwing exceptions** across226 service/repository/view-model boundaries so callers can't forget to227 catch (see `references/design-patterns.md`).228- **Write tests that exploit the architecture**: fake the repository to229 test a ViewModel, fake the service to test a repository. If a class is230 hard to fake or mock, that's a signal the class has too many231 responsibilities or unclear inputs/outputs.232233## Reference files — read these for implementation detail234235| File | Read when you need... |236|---|---|237| `references/ui-layer.md` | Full View/ViewModel code patterns, UI state, `ListenableBuilder` wiring, the Command pattern in context, and disposing ViewModels/Commands/subscriptions |238| `references/data-layer.md` | Full Repository/Service code patterns, domain vs. API models, the `Result` pattern in context |239| `references/dependency-injection.md` | Wiring everything together with `provider` + `go_router`, the full rules-of-engagement table |240| `references/migration.md` | Converting a `StatefulWidget`/`setState()` screen into View + ViewModel + Repository, step by step |241| `references/testing.md` | Unit-testing ViewModels/Repositories with fakes, widget-testing Views |242| `references/audit-checklist.md` | A quick-scan list of architecture smells for reviewing existing code against this skill's rules |243| `references/design-patterns.md` | Full `Command`/`Result` class implementations, Optimistic State, key-value persistence, SQL persistence, offline-first strategies |244| `references/recommendations.md` | The Flutter team's full prioritized do/don't tables, naming conventions, and recommended packages/resources |245246Load only the reference files relevant to the current task — don't read all247of them for a small, single-widget request.