Jede Schicht hat eine Aufgabe und kennt nur die Schicht darunter.
Der Screen ist immer in genau einem von drei Zuständen.
sealed interface WeatherUiState {
data object Loading : WeatherUiState
data class Success(
val city: City,
val weather: CurrentWeather,
) : WeatherUiState
data class Error(val message: String) : WeatherUiState
}Das ViewModel hält den Zustand. Es überlebt das Drehen des Bildschirms.
class WeatherViewModel : ViewModel() {
private val _uiState = MutableStateFlow<WeatherUiState>(
WeatherUiState.Success(
city = fakeCities.first(),
weather = fakeCurrentWeather.getValue("Lörrach"),
)
)
val uiState: StateFlow<WeatherUiState> = _uiState.asStateFlow()
}Ein Composable holt den Zustand aus dem ViewModel, ein zweites zeigt ihn nur an.
@Composable // stateful
fun WeatherScreen(viewModel: WeatherViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
WeatherContent(uiState = uiState)
}
@Composable // stateless
fun WeatherContent(uiState: WeatherUiState) {
// Scaffold, TopAppBar ...
when (uiState) { /* Loading, Success, Error */ }
}Das Repository liefert die Daten. Das ViewModel kennt nur das Interface.
interface WeatherRepository {
suspend fun getCurrentWeather(city: City): CurrentWeather
}class FakeWeatherRepository(
private val shouldFail: Boolean = false,
) : WeatherRepository {
override suspend fun getCurrentWeather(city: City): CurrentWeather {
delay(1500) // simuliert die Antwortzeit eines Servers
if (shouldFail) throw IOException("Keine Verbindung (simuliert)")
return fakeCurrentWeather[city.name]
?: throw IOException("Keine Wetterdaten für ${city.name}")
}
}suspend-Funktionen können warten, ohne die Oberfläche zu blockieren. Das ViewModel startet sie in viewModelScope.
fun loadWeather(city: City) {
_uiState.value = WeatherUiState.Loading
viewModelScope.launch {
_uiState.value = try {
val weather = repository.getCurrentWeather(city)
WeatherUiState.Success(city, weather)
} catch (e: CancellationException) { throw e }
catch (e: Exception) {
WeatherUiState.Error(e.message ?: "Unbekannter Fehler")
}
}
}Das ViewModel bekommt sein Repository als Parameter mit Default-Wert. Für die echte API ändert sich später nur diese Zeile.
class WeatherViewModel(
private val repository: WeatherRepository = FakeWeatherRepository(),
) : ViewModel()