ViewModel und Architektur

15 MinutenZustand und Daten sauber von der Oberfläche trennen

MVVM-Schichten

Jede Schicht hat eine Aufgabe und kennt nur die Schicht darunter.

MVVM-Schichten der Wetter-App
MVVM-Schichten der Wetter-App

UiState als sealed class

Der Screen ist immer in genau einem von drei Zuständen.

sealed class WeatherUiState {
  const WeatherUiState();
}

class WeatherLoading extends WeatherUiState { /* ... */ }

class WeatherSuccess extends WeatherUiState {
  const WeatherSuccess(this.city, this.weather);
  final City city;
  final CurrentWeather weather;
}

class WeatherError extends WeatherUiState { /* final String message */ }

ViewModel mit ChangeNotifier

Das ViewModel hält den Zustand und meldet Änderungen mit notifyListeners().

class WeatherViewModel extends ChangeNotifier {
  final WeatherUiState _uiState = WeatherSuccess(
    fakeCities.first,
    fakeCurrentWeather[fakeCities.first.name]!,
  );

  WeatherUiState get uiState => _uiState;
}

Stateful und stateless

Ein Widget besitzt das ViewModel, ein zweites zeigt den Zustand nur an.

class _WeatherScreenState extends State<WeatherScreen> { // stateful
  final _viewModel = WeatherViewModel();

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: _viewModel,
      builder: (context, _) => WeatherContent(uiState: _viewModel.uiState),
    );
  }
}

class WeatherContent extends StatelessWidget { /* ... */ } // stateless

Repository

Das Repository liefert die Daten. Das ViewModel kennt nur das Interface.

abstract interface class WeatherRepository {
  Future<CurrentWeather> getCurrentWeather(City city);
}

Repository

class FakeWeatherRepository implements WeatherRepository {
  FakeWeatherRepository({this.shouldFail = false});
  final bool shouldFail;

  @override
  Future<CurrentWeather> getCurrentWeather(City city) async {
    await Future.delayed(const Duration(milliseconds: 1500));
    if (shouldFail) throw Exception('Keine Verbindung (simuliert)');
    final weather = fakeCurrentWeather[city.name];
    if (weather == null) throw Exception('Keine Wetterdaten für ${city.name}');
    return weather;
  }
}

async/await im ViewModel

Das ViewModel setzt den Zustand vor und nach dem Warten und meldet jede Änderung.

Future<void> loadWeather(City city) async {
  _uiState = const WeatherLoading();
  notifyListeners();
  try {
    final weather = await _repository.getCurrentWeather(city);
    _uiState = WeatherSuccess(city, weather);
  } catch (e) {
    _uiState = WeatherError('$e');
  }
  notifyListeners();
}

Repository als Default-Parameter

Das ViewModel bekommt sein Repository als optionalen Parameter. Für die echte API ändert sich später nur diese Zeile.

WeatherViewModel({WeatherRepository? repository})
  : _repository = repository ?? FakeWeatherRepository();