Clean MVVM describes responsibilities rather than merely folder names. The view displays state and accepts actions. A view model owns screen state. A service coordinates a domain workflow. A repository describes access to persisted data. We first test this separation without Flutter. Vehicle File uses Riverpod for wiring and different data implementations for different platforms.
01Define the contract
VehicleRepository describes save and all. The service depends on this contract. MemoryRepository is only a learning adapter. Its data disappears when the program stops.
abstract interface class VehicleRepository {
Future<void> save(String name);
Future<List<String>> all();
}
class MemoryRepository implements VehicleRepository {
final values = <String>[];
@override
Future<void> save(String name) async {
values.add(name);
}
@override
Future<List<String>> all() async => List.unmodifiable(values);
}
class VehicleService {
VehicleService(this.repository);
final VehicleRepository repository;
Future<void> create(String input) async {
final name = input.trim();
if (name.isEmpty) throw ArgumentError('Name required');
await repository.save(name);
}
}
class VehicleViewModel {
VehicleViewModel(this.service);
final VehicleService service;
String message = '';
Future<void> submit(String input) async {
try {
await service.create(input);
message = 'Saved';
} catch (error) {
message = error.toString();
}
}
}
Future<void> main() async {
final repository = MemoryRepository();
final model = VehicleViewModel(VehicleService(repository));
await model.submit(' ');
print(model.message);
await model.submit(' Family car ');
print(await repository.all());
}
Run / check
dart run architecture.dart02Keep the rule in the service
VehicleService normalises the name and rejects empty input. This applies whether a form, import or test later calls the workflow. The service does not access BuildContext.
03Represent screen state
VehicleViewModel translates success and failure into visible state. This small model stores only a message. A real interface also needs loading state and protection against concurrent write actions.
04Replace the adapter
Replace only the repository implementation. The service should retain its rules. Then compare the linked app files: some copied template components still use Meter names. The vehicle domain is expressed through its fields and workflows rather than a retrospectively invented class name.
An empty name displays Name required. The confirmed name is stored as Family car.
Your exercise
Implement a repository whose save throws StateError. Check whether Saved is still displayed.
Show the solution and explanation
The error reaches the view model through await. Saved is not set. A real interface should display a translated message and retain the draft. Raw error.toString is used only for learning here.
Project context and sources
The example is simplified for learning. The linked source version shows its use in the project and the checks applied there.