State
Bisher zeigt die App nur feste Daten an. Jetzt soll die Oberfläche
auf Eingaben reagieren: Ein Umschalter in der TopAppBar wechselt die
Temperaturanzeige zwischen °C und °F.
Code: schritt-05 ·
Änderungen
Erster Versuch
Eine normale Variable reicht nicht: Antippen ändert den Wert, aber die Anzeige bleibt gleich.
@Composable
fun WeatherScreen(/* ... */) {
var useCelsius = true // funktioniert nicht!
TextButton(onClick = { useCelsius = !useCelsius }) {
Text(text = if (useCelsius) "°C" else "°F")
}
}Compose weiß nicht, dass sich useCelsius geändert hat, und zeichnet
das Composable deshalb nicht neu. Selbst wenn es neu gezeichnet würde,
stünde useCelsius danach wieder auf true, weil die Funktion von
vorne ausgeführt wird.
Recomposition
Ändert sich State, ruft Compose die betroffenen Composables erneut auf. Das heißt Recomposition.
mutableStateOf(…): ein Wert, dessen Änderungen Compose beobachtetremember { … }: behält den Wert über Recompositions hinwegrememberSaveable { … }: behält ihn auch beim Drehen des Bildschirms
var useCelsius by remember { mutableStateOf(true) }Merksatz: UI = f(State). Die Oberfläche ist eine Funktion des Zustands; ändert sich der State, wird sie neu berechnet.
Recomposition lässt sich beobachten, z. B. in CurrentWeatherCard, die
useCelsius als Parameter bekommt (siehe unten): dort
Log.d("CurrentWeatherCard", "Recomposition") einfügen, die App starten
und im Fenster Logcat von Android Studio danach filtern. Jedes
Umschalten erzeugt eine neue Zeile. Compose kann unveränderte Bereiche
überspringen. Neu ausgeführt werden vor allem Bereiche, die geänderten
State lesen oder geänderte Parameter bekommen. Ein Log.d direkt in
WeatherScreen erscheint deshalb nicht unbedingt.
Durch by (Delegation) schreiben wir useCelsius statt
useCelsius.value. Dafür braucht es zwei Imports, die Android Studio
nicht immer automatisch vorschlägt:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValueBeim Drehen des Bildschirms wird die Activity neu erzeugt, und mit
remember geht der Wert verloren. Mit rememberSaveable bleibt er
erhalten. Gut im Emulator auszuprobieren: einmal mit remember, einmal
mit rememberSaveable auf °F schalten und das Gerät drehen.
State Hoisting
Den State dort halten, wo alle ihn brauchen, und ihn nach unten weitergeben. Änderungen kommen als Events zurück nach oben.
Der Umschalter (UnitToggle) und die Karte (CurrentWeatherCard)
brauchen beide useCelsius. Deshalb liegt der State im gemeinsamen
Eltern-Composable WeatherScreen. Dieses Muster heißt
State Hoisting („State nach oben ziehen“), das Prinzip dahinter
Unidirectional Data Flow: State fließt nach unten, Events nach oben.
var useCelsius by rememberSaveable { mutableStateOf(true) }
UnitToggle(
useCelsius = useCelsius, // State ↓
onToggle = { useCelsius = !useCelsius }, // Event ↑
)
CurrentWeatherCard(city, weather, useCelsius) // State ↓Vollständig in WeatherScreen: UnitToggle steckt in der TopAppBar,
die Karte im Inhalt des Scaffold.
var useCelsius by rememberSaveable { mutableStateOf(true) }
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = city.name) },
actions = {
UnitToggle(
useCelsius = useCelsius,
onToggle = { useCelsius = !useCelsius },
)
},
)
},
) { innerPadding ->
CurrentWeatherCard(city, weather, useCelsius, /* ... */)
}CurrentWeatherCard bekommt dafür einen neuen Parameter
useCelsius: Boolean und zeigt die Temperatur mit
formatTemperature(weather.temperature, useCelsius) an (siehe unten).
UnitToggle steht ebenfalls in ui/WeatherScreen.kt. Es ist stateless
und bekommt den aktuellen Wert und meldet
das Antippen über onToggle:
@Composable
fun UnitToggle(useCelsius: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier) {
TextButton(onClick = onToggle, modifier = modifier) {
Text(text = if (useCelsius) "°C" else "°F")
}
}Die Umrechnung steckt in einer Hilfsfunktion in ui/WeatherConditionUi.kt:
fun formatTemperature(celsius: Double, useCelsius: Boolean): String =
if (useCelsius) {
"${celsius.roundToInt()} °C"
} else {
"${(celsius * 9 / 5 + 32).roundToInt()} °F"
}Zusammenfassung
State in Compose: mutableStateOf für beobachtbare Werte,
remember/rememberSaveable zum Behalten, State so weit oben wie nötig,
Events als Lambdas nach oben.
Dasselbe Muster findet sich in Flutter und React Native wieder, nur mit anderen Namen. In der nächsten Lektion wandert der Zustand des ganzen Screens noch eine Ebene höher: in ein ViewModel.