Sparround

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.

QuestionThe guide's answer
The relationship between view and view modelOne-to-one
View model to repositoryMany-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 stateIn a repository — it is the single source of truth for data and the ideal place for app-wide lifecycle state
Screen-scoped UI stateIn 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".

dart
// 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