In a Flutter app following the architecture guide's MVVM, how does a tap reach the repository, and how does the new state reach the widgets?
answer
- events down, state up
- view calls a view model method
- repository mutates the data
- notifyListeners on ChangeNotifier
- ListenableBuilder re-runs builder
basics
~20 sThe widget's callback calls a view model method, which asks the repository to change the data; the repository returns new data, the view model updates its state and calls notifyListeners(), and a ListenableBuilder rebuilds the view.
solid answer
~40 sThe guide calls this **unidirectional data flow**. A gesture in the view invokes a view model method; the view model calls the repository, which is the only place that data changes; the repository returns the new domain data; the view model stores new UI state and, in the guide's sample, calls `notifyListeners()` because it extends `ChangeNotifier`. The view wraps its subtree in `ListenableBuilder(listenable: viewModel, builder: ...)`, which re-runs `builder` on each notification. Data can also start in the repository, such as polled fares, and then only travels the second half. The view never mutates the view model's data; the guide exposes lists as an `UnmodifiableListView`.
code
dart · 44 linesimport 'dart:collection';
import 'package:flutter/material.dart';
abstract class FareWatchRepository {
Future<void> watch(String offerId);
Future<Set<String>> watchedIds();
}
class FlightResultsViewModel extends ChangeNotifier {
FlightResultsViewModel({required FareWatchRepository watches})
: _watches = watches;
final FareWatchRepository _watches;
List<String> _watched = [];
UnmodifiableListView<String> get watched => UnmodifiableListView(_watched);
Future<void> watchFare(String offerId) async {
await _watches.watch(offerId); // the change happens in the repository
_watched = (await _watches.watchedIds()).toList();
notifyListeners(); // state flows back up
}
}
class WatchedFaresView extends StatelessWidget {
const WatchedFaresView({super.key, required this.viewModel});
final FlightResultsViewModel viewModel;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, _) => Column(
children: [
for (final id in viewModel.watched) Text(id),
TextButton(
onPressed: () => viewModel.watchFare('LHR-JFK-0915'),
child: const Text('Watch fare'),
),
],
),
);
}
}go deeper
Recite the five steps: tap, view model, repository, view model state, rebuild. Name ChangeNotifier and ListenableBuilder as the sample's tools.
Explain why only the repository changes data, what UnmodifiableListView does and does not protect, and what ListenableBuilder's child saves.
Diagnose flows that break the direction, such as a view writing to view model state or a repository holding a BuildContext, and the stale-data bugs they cause.
Discuss keeping the same one-way flow when a team swaps ChangeNotifier for streams, Riverpod or BLoC across features.
## The loop the guide describes Flutter's architecture guide recommends **unidirectional data flow**: state moves from the data layer up to the widgets, and user events move the other way. Its loop has five steps: 1. **UI layer.** A gesture fires a widget callback, which calls a method on the view model. 2. **View model.** It calls the repository method that knows how to change the data. 3. **Data layer.** The repository changes the data, if needed, and returns the new data. 4. **View model.** It stores its new UI state and notifies the view. 5. **UI layer.** The view rebuilds and shows the new state. The key rule is that **data changes happen only in the repository**, the source of truth. A view model that edits its own copy and hopes to sync later has broken the flow. ## Flutter's pieces in that loop The guide's sample uses only SDK classes: | Step | Flutter mechanism | |---|---| | View calls the view model | an `onPressed` or `onDismissed` callback calling a view model method | | View model announces a change | it `extends ChangeNotifier` and calls `notifyListeners()` | | View reacts | `ListenableBuilder(listenable: viewModel, builder: ...)` | | View cannot mutate state | private fields plus getters; lists exposed as `UnmodifiableListView` | `ChangeNotifier` is a Flutter foundation class that implements `Listenable`. `ListenableBuilder` subscribes to any `Listenable` and calls its `builder` again whenever the listenable notifies. Its optional `child` argument is built once and passed into `builder`, so an expensive subtree that does not depend on the view model is not rebuilt. The guide also names Riverpod, BLoC and streams as valid replacements for `ChangeNotifier`; the direction of flow is the part that does not change. ## A flight-search round trip The user taps a star to watch a fare: - `FlightResultsScreen` calls `viewModel.watchFare(offerId)`. - `FlightResultsViewModel.watchFare` calls `_fareWatchRepository.watch(offerId)`. - `FareWatchRepository` sends the change through its service and updates its cached set of watched ids. - The view model reloads its watched ids from the repository and calls `notifyListeners()`. - The `ListenableBuilder` around the results list re-runs `builder`, and the star fills in. When the repository instead polls the fares API and sees a lower price, only the second half runs: repository to view model, then `notifyListeners()`, then a rebuild. ## Why the view gets an unmodifiable view The guide's sample exposes its list through a getter that returns `UnmodifiableListView(_bookings)`. That wrapper, from `dart:collection`, throws if the view tries to add or remove items, so the view cannot sneak a change past the view model and repository. It is a *view* over the private list, not a copy and not deep immutability: the view model's own changes show through, and the elements themselves are only as immutable as their classes. That is why the guide also recommends **immutable data models**. ## What breaks the direction - A widget writes to `viewModel.offers` directly and then asks for a rebuild. - A repository holds a reference to a widget or a `BuildContext` to push updates. - A view model keeps an edited copy of repository data as the truth. - A view calls a service to save something and the repository never hears about it.
- What is ListenableBuilder's child parameter for?`child` is a widget built once by the parent and handed to `builder` on every notification. Put a subtree there when it does not depend on the view model, so each `notifyListeners()` re-runs only the part that reads its state.
- How would polled data from the repository reach a view model that is not calling it?The repository exposes a `Stream` (or is itself a `Listenable`) and the view model subscribes, updates its state on each event and calls `notifyListeners()`. The guide's example is a user-profile repository whose stream emits on sign-in and sign-out.
saying these in an interview costs you the question
- The view should edit the view model's list and then call notifyListeners.
- UnmodifiableListView makes the list's elements immutable too.
- Repositories push updates by calling setState on the widgets.
- ListenableBuilder also rebuilds the widget passed as its child.
- A view model may keep its edited copy as the truth and sync later.