# Flutter Mvvm Architecture

> 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.

- Skill: `reza00farjam/flutter-mvvm-architecture` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add reza00farjam/flutter-mvvm-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/reza00farjam/flutter-mvvm-architecture/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- License: MIT
- Author: reza00farjam (https://skillmd.com/u/reza00farjam)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/reza00farjam/flutter-mvvm-architecture

---


# 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](https://docs.flutter.dev/app-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](https://github.com/flutter/samples/tree/main/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

1. **Identify the feature** — it's exactly one View + one ViewModel pair.
   Name them `<Feature>Screen`/`<Feature>View` and `<Feature>ViewModel`.
2. **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.
3. **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.
4. **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.
5. **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(...)`.
6. **Wire dependency injection** at the app/router level — don't
   instantiate repositories or services inside widgets or view models.
7. **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.
8. **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.

