The official architecture: which layer state lives in
The Flutter team's official architecture guide defines two main layers:
UI layer
- View — a composition of widgets. Per the guide a view should hold only minimal logic: simple if-statements to show or hide widgets based on a flag, animation logic, layout logic based on device information, and simple routing. All logic related to data should be handled in the view model.
- View model — converts repository data into UI state and maintains the current state so the view can rebuild without losing data. It exposes callbacks called commands to the view.
Data layer
- Repository — the source of truth for model data: caching, error handling, retries, and transforming raw data into domain models.
- Service — wraps external APIs (REST, platform, files) and holds no state.
An optional domain layer — use cases/interactors: when data from several repositories is needed, when the logic is exceedingly complex, or when it is reused across view models.
| Question | The guide's answer |
|---|---|
| The relationship between view and view model | One-to-one |
| View model to repository | Many-to-many; one repository can serve several view models |
| Do repositories know about each other? | No. If you need data from two repositories, combine it in the view model or the domain layer |
| App-wide session state | In a repository — it is the single source of truth for data and the ideal place for app-wide lifecycle state |
| Screen-scoped UI state | In the view model — so it survives the view rebuilding |
The guide equates this structure with MVVM: "If you've encountered the Model-View-ViewModel architectural pattern (MVVM), this will be familiar." An important nuance: the guide does not prescribe a state management library — the principles apply equally to Provider, Riverpod and BLoC. In an interview that is a strong position: "architecture comes before the library choice".
// Data layer: service state saxlamır.
class BookingApiClient {
Future<List<BookingDto>> fetch() async { /* ... */ }
}
// Data layer: repository — həqiqətin mənbəyi, domain modelinə çevirir.
class BookingRepository {
BookingRepository({required BookingApiClient apiClient})
: _apiClient = apiClient;
final BookingApiClient _apiClient; // asılılıq private saxlanılır
Future<List<Booking>> loadAll() async {
final dtos = await _apiClient.fetch();
return dtos.map(Booking.fromDto).toList();
}
}
// UI layer, variant A — Provider: ChangeNotifier view model.
class BookingViewModel extends ChangeNotifier {
BookingViewModel({required BookingRepository repository})
: _repository = repository;
final BookingRepository _repository;
// ...
}
// UI layer, variant B — Riverpod: AsyncNotifier view model.
class BookingNotifier extends AsyncNotifier<List<Booking>> {
@override
Future<List<Booking>> build() =>
ref.watch(bookingRepositoryProvider).loadAll();
}
// UI layer, variant C — BLoC: view model rolunda bloc.
class BookingBloc extends Bloc<BookingEvent, BookingState> {
BookingBloc(this._repository) : super(const BookingState()) {
on<BookingStarted>((event, emit) async { /* ... */ });
}
final BookingRepository _repository;
}The same architecture in three libraries — only the notifier's type changes.
📚 Sources and documentation
- Flutter architecture guideofficialdocs.flutter.dev
The source of every rule in this topic: the layers, their relationships, how much logic a view may hold.
- Architecture conceptsofficialdocs.flutter.dev
- Case study: the Compass appofficialdocs.flutter.dev
How the principles look in a real codebase.
- The command patternofficialdocs.flutter.dev
The official formalisation of the callbacks a view model exposes to the view.