skip to content

In Flutter's official architecture guide, what are views, view models, repositories and services, and which layer does each belong to?

level: juniorimportance: must knowfreq 55%

answer

  1. two broad layers, four components
  2. a composition of widgets
  3. UI state plus callbacks
  4. source of truth per data type
  5. stateless wrapper per data source

basics

~20 s

Flutter's architecture guide puts views (compositions of widgets) and view models (UI state and UI logic) in the UI layer, and repositories (the source of truth per data type) and services (stateless wrappers around external APIs) in the data layer.

solid answer

~40 s

The guide splits an app into a **UI layer** and a **data layer**. In the UI layer, a **view** is the set of widgets that makes up a feature, usually a screen with a `Scaffold`, and a **view model** turns app data into UI state and exposes callbacks the view wires to gestures; views and view models pair one-to-one. In the data layer, a **repository** is the source of truth for one type of data and handles caching, retries, errors and mapping into domain models, while a **service** wraps one external data source such as a REST API, a local file or a platform plugin and holds no state. In MVVM terms, repositories and services together are the Model.

code

dart · 71 lines
dart
import 'package:flutter/material.dart';

// Domain model: what the rest of the app sees.
class FlightOffer {
  const FlightOffer({required this.carrier, required this.priceCents});
  final String carrier;
  final int priceCents;
}

// Service: stateless, one per data source, raw data out.
class FlightApiService {
  Future<List<Map<String, Object?>>> searchOffers(String route) async {
    return []; // HTTP call omitted
  }
}

// Repository: source of truth for flight offers.
class FlightRepository {
  FlightRepository({required FlightApiService api}) : _api = api;
  final FlightApiService _api;

  Future<List<FlightOffer>> search(String route) async {
    final raw = await _api.searchOffers(route);
    return [
      for (final json in raw)
        FlightOffer(
          carrier: json['carrier'] as String,
          priceCents: json['price_cents'] as int,
        ),
    ];
  }
}

// View model: UI state for one view; the repository stays private.
class FlightSearchViewModel extends ChangeNotifier {
  FlightSearchViewModel({required FlightRepository flights})
      : _flights = flights;
  final FlightRepository _flights;

  List<FlightOffer> _offers = const [];
  List<FlightOffer> get offers => _offers;

  Future<void> search(String route) async {
    _offers = await _flights.search(route);
    notifyListeners();
  }
}

// View: widgets that render the view model's state.
class FlightSearchScreen extends StatelessWidget {
  const FlightSearchScreen({super.key, required this.viewModel});
  final FlightSearchViewModel viewModel;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListenableBuilder(
        listenable: viewModel,
        builder: (context, _) => ListView(
          children: [
            for (final offer in viewModel.offers)
              ListTile(
                title: Text(offer.carrier),
                subtitle: Text('${offer.priceCents / 100}'),
              ),
          ],
        ),
      ),
    );
  }
}

go deeper

for a junior

Name the two layers and the four components, and give one job for each. A flight-search example with one class per component is enough.

for a middle

Explain the relationships: one-to-one for view and view model, many-to-many below that, and no repository-to-repository calls. Say where caching and mapping live.

for a senior

Show how the boundaries pay off: a view model testable with a fake repository, a repository swappable per environment, and no widget that knows an HTTP client exists.

for a principal

Discuss when to deviate: small CRUD apps that skip services or use cases, and how to keep the team consistent once the guide is adapted.

## Two layers, four components The Flutter team's **architecture guide** on docs.flutter.dev names **separation of concerns** as the most important principle and recommends splitting an app into two broad layers, each with two kinds of class: | Layer | Component | Job | |---|---|---| | UI layer | **View** | The widgets that present one feature and forward user events | | UI layer | **View model** | Converts app data into UI state and exposes callbacks for the view | | Data layer | **Repository** | Source of truth for one type of data; caching, retry, error handling, mapping | | Data layer | **Service** | Wraps one external data source and exposes `Future`s and `Stream`s | If you know the MVVM pattern, the mapping is direct: views and view models are MVVM's View and ViewModel, while **repositories and services together form the Model**. The guide calls these recommendations, not rules, and expects teams to adapt them. ## The UI layer A **view** in Flutter is not one widget. It is a *composition* of widgets that makes a feature: often a screen with its own route and a `Scaffold`, but sometimes a small reusable piece. The guide's example is a logout button with its own `LogoutViewModel` that can be dropped into a menu or a settings screen. A view may hold only simple logic: - `if` checks that show or hide widgets based on a flag or nullable field in the view model - animation logic - layout decisions based on screen size or orientation - simple routing A **view model** holds most of the app's logic. It reads domain models from repositories, filters, sorts or combines them into what the view needs, keeps UI state (for example which tab of a carousel is active) so the view can rebuild without losing it, and exposes callbacks the guide calls **commands**. Views and view models have a **one-to-one** relationship. ## The data layer A **repository** is the **source of truth** for one type of app data, so there is one repository per data type. It polls services, transforms raw data into **domain models**, and owns caching, error handling, retry logic and refreshing. Because it is the source of truth it is also where the guide puts app-wide session state such as the signed-in user. A **service** is the lowest layer. It wraps one data source (a REST endpoint, the underlying iOS or Android APIs through a plugin, local files) and returns asynchronous results. The guide says services hold no state and recommends **one service per data source**. ## How the pieces relate 1. View to view model: one-to-one. 2. View model to repository: many-to-many. One view model reads several repositories; one repository serves many view models. 3. Repository to service: many-to-many. 4. Repository to repository: **none**. Repositories should never be aware of each other; data from two repositories is combined in a view model or in a use case. The guide also describes an **optional domain layer** of use cases for logic that is complex, merges several repositories or is reused by several view models. Most apps can skip it. ## A flight-search example In a flight-search app, `FlightSearchScreen` is the view and `FlightSearchViewModel` extends `ChangeNotifier` to hold the visible offers and the current filters. `FlightRepository` is the source of truth for flight offers: it caches results per route and maps raw payloads into `FlightOffer` domain models. `FlightApiService` wraps the fares HTTP API, and a separate storage service might wrap saved searches on disk. The screen never touches the service; the view model never parses JSON. ## Why interviewers ask it The question checks whether a candidate can say what each class may know. A view that calls an HTTP client, a service that caches, or a repository that reads another repository all break the guide's boundaries, and those boundaries are what make each class testable with a fake of the layer below it.

  • Is a view in Flutter's architecture guide always a whole screen?
    No. A view is often a screen with its own route and `Scaffold`, but the guide's rule is one view model per *collection of widgets*, not per screen. Its example is a logout button with its own `LogoutViewModel` that can sit inside a menu or a settings page. On large screens several views can share one screen.
  • Where does app-wide session state, such as the signed-in user, live in this architecture?
    In a repository. The guide calls repositories the natural home for app-wide lifecycle state because they are the source of truth and have a many-to-many relationship with view models. One repository instance is shared by several view models, which observe it through streams or methods without breaking the one-to-one view and view model pairing.

saying these in an interview costs you the question

  • A view is exactly one widget class.
  • Services cache responses and retry; repositories just forward calls.
  • View models may call services directly to skip the repository.
  • The domain layer of use cases is required in every Flutter app.
  • In MVVM terms, the view model is the Model and repositories are extra.