A Flutter FlightSearchViewModel exposes its repository as a public field, calls FlightApiService directly, and its FlightRepository calls TripRepository; which architecture-guide rules does this break, and how would you fix it?
answer
- what the view can reach
- private repositories on view models
- no view model to service calls
- repositories never know each other
- abstract repository, several implementations
basics
~20 sIt breaks three rules: repositories stay private to view models, view models never call services, and repositories never know each other. Make the fields private, go through the repository, and merge trip data in the view model or a use case.
solid answer
~40 sThree violations. A **public repository field** lets the view reach the data layer, so the guide keeps repositories private on view models. A **view model calling `FlightApiService`** skips the repository, the source of truth, along with its cache, retry and mapping; the call belongs in `FlightRepository`, with the service private there. **`FlightRepository` calling `TripRepository`** breaks the rule that repositories are never aware of each other; the view model, or a use case if the merge is reused or complex, reads both. While fixing it, declare `FlightRepository` as an **abstract class** with remote and local implementations, which the guide strongly recommends: it lets you swap environments and hand the view model a fake in tests.
code
dart · 43 linesimport 'package:flutter/foundation.dart';
class FlightOffer {
const FlightOffer(this.id);
final String id;
}
class Trip {
const Trip(this.offerId);
final String offerId;
}
abstract class FlightRepository {
Future<List<FlightOffer>> search(String route);
}
abstract class TripRepository {
Future<List<Trip>> savedTrips();
}
class FlightSearchViewModel extends ChangeNotifier {
FlightSearchViewModel({
required FlightRepository flights,
required TripRepository trips,
}) : _flights = flights,
_trips = trips;
// Private: the view cannot reach the data layer.
final FlightRepository _flights;
final TripRepository _trips;
List<FlightOffer> _offers = const [];
Set<String> _savedIds = const {};
List<FlightOffer> get offers => _offers;
bool isSaved(FlightOffer o) => _savedIds.contains(o.id);
Future<void> search(String route) async {
_offers = await _flights.search(route);
// Two repositories combined here, not inside FlightRepository.
_savedIds = {for (final t in await _trips.savedTrips()) t.offerId};
notifyListeners();
}
}go deeper
Spot that a view model should not hold a service and that its repositories should be private.
Name all three broken rules and say what each bypass skips: caching, retry, mapping.
Turn the review into lasting guidance: abstract repositories, per-environment implementations and fakes, plus a checklist the team can apply to every pull request.
Decide how to enforce these boundaries at scale, with lint rules, import restrictions or package splits, and what that costs the team.
## The code under review A pull request adds flight search: ```dart class FlightSearchViewModel extends ChangeNotifier { FlightSearchViewModel(this.flights, this.api); final FlightRepositoryImpl flights; // public, concrete final FlightApiService api; // a service in the UI layer Future<void> search(String route) async { final offers = await api.searchOffers(route); // bypasses the repository // ... } } class FlightRepositoryImpl { FlightRepositoryImpl(this._api, this._trips); final FlightApiService _api; final TripRepository _trips; // repository knows a repository } ``` Each problem maps to one line of Flutter's architecture guide. ## Violation 1: the view can reach the data layer The guide's UI-layer case study says repositories should be **private members** of the view model; otherwise views have direct access to the data layer. With `flights` public, `FlightSearchScreen` can write `viewModel.flights.search(...)` and bypass the view model's UI state. **Fix:** private fields assigned from named constructor parameters. ## Violation 2: a view model talks to a service The layers talk only to their neighbours: views to view models, view models to repositories (or use cases), repositories to services. The data-layer case study keeps the service private inside the repository so the UI **cannot bypass the repository**. Calling `api.searchOffers` from the view model skips: - the repository's cache, so every screen refetches; - its retry and error handling; - the mapping from API models to domain models, so the view model now depends on the server's payload shape. **Fix:** remove the service from the view model; add a method to `FlightRepository` if it lacks one. ## Violation 3: repositories that know each other The guide says repositories should **never be aware of each other**. If trips and flights must be combined, that happens in the view model or, when complex or reused, in a **use case**. A flight repository that calls a trip repository creates hidden ordering between data types and makes each harder to test. **Fix:** drop `TripRepository` from the constructor; let the view model read both. ## The improvement worth adding: abstract repositories The guide **strongly** recommends **abstract repository classes**. Its sample declares `BookingRepository` as a base class with `BookingRepositoryRemote` for the real server and `BookingRepositoryLocal` for local development, and the same shape serves tests: a fake implements the abstract class. | Before | After | |---|---| | View model depends on `FlightRepositoryImpl` | View model depends on abstract `FlightRepository` | | One implementation, tied to the network | `FlightRepositoryRemote` and `FlightRepositoryLocal` | | Test needs the real API | Test passes a fake `FlightRepository` | ## Review checklist 1. Does any widget import a repository or service? It should see only its view model. 2. Are repositories private on view models, and services private on repositories? 3. Does any repository take another repository as a dependency? 4. Do view models depend on abstract repository types? 5. Is data changed only in repositories, with the view model notifying afterward? How the view model receives its repositories, through provider, get_it or a plain constructor, is a separate wiring question; the rules above hold whichever tool does the wiring.
- Why does the guide recommend abstract repository classes even when there is one backend?An abstract `FlightRepository` lets the app ship a remote implementation, run a local one during development or staging, and hand a fake to view model tests, all without changing the view model. The guide rates it strongly recommended for that reason.
- The same trips-plus-flights merge now appears in three view models. What changes?That meets the guide's reuse trigger for a use case. Move the merge into a use case that depends on both repositories and have each view model depend on it, while still reading other repositories directly where it needs nothing more.
saying these in an interview costs you the question
- Public repository fields are fine because the view never uses them.
- Calling the service from the view model is faster, so it is acceptable.
- Repositories should inject each other to keep view models thin.
- Abstract repository classes only matter if you use a mocking library.
- The fix is to move the HTTP call into the widget's initState.