rememberUpdatedState in Compose – wenn dein Callback im Effekt einfriert
Ein LaunchedEffect mit key1 = Unit startet genau einmal – und hält damit für immer die Lambda-Referenz vom ersten Durchlauf fest. Klickt der Nutzer später etwas anderes an, passiert das Falsche. rememberUpdatedState löst genau das.
Der Fehler sieht harmlos aus und ist deshalb so gemein: Ein Effekt soll genau einmal starten, bekommt also Unit als Schlüssel – und ruft am Ende ein Lambda auf, das von außen hereingereicht wurde. Sobald sich dieses Lambda ändert, ruft der Effekt trotzdem noch das alte auf. Ich habe das selbst schon zweimal gebaut, bevor ich verstanden habe, warum.
Der Fehler
Ein Splash-Screen, der nach drei Sekunden weiterspringt:
@Composable
fun Splash(onTimeout: () -> Unit) {
LaunchedEffect(Unit) { // absichtlich nur EINMAL starten
delay(3_000)
onTimeout() // ⚠️ das Lambda vom ERSTEN Durchlauf
}
Text("Moment …")
}LaunchedEffect(Unit) tut genau das, was draufsteht: Der Block läuft einmal an und wird beim Neuzeichnen nicht neu gestartet. Damit bleibt aber auch die Referenz auf onTimeout eingefroren, so wie sie beim ersten Durchlauf war. Rendert der Aufrufer neu und übergibt ein anderes Lambda – etwa weil der Ziel-Bildschirm inzwischen ein anderer ist –, landet der Nutzer nach drei Sekunden am falschen Ort.
Der naheliegende „Fix" macht es schlimmer:
LaunchedEffect(onTimeout) { … } // ❌ Timer startet bei jeder neuen Lambda-Instanz neuEin Lambda, das im Aufrufer inline geschrieben ist, ist bei jeder Recomposition ein neues Objekt. Der Schlüssel ändert sich also ständig, der Effekt wird abgebrochen und neu gestartet – und die drei Sekunden fangen jedes Mal von vorne an. Im schlimmsten Fall laufen sie nie ab.
Die Lösung
rememberUpdatedState gibt dir ein State-Objekt, dessen Wert bei jeder Recomposition auf den aktuellen aktualisiert wird, ohne dass sich das Objekt selbst ändert. Der Effekt liest darüber immer den frischen Wert und startet trotzdem nie neu:
@Composable
fun Splash(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) { // läuft weiterhin nur EINMAL
delay(3_000)
currentOnTimeout() // ✅ immer das aktuelle Lambda
}
Text("Moment …")
}Die Aufteilung ist der ganze Trick: Der Schlüssel entscheidet, wann der Effekt neu startet. rememberUpdatedState entscheidet, welchen Wert er dabei sieht. Beides voneinander zu trennen ist genau das, was hier fehlte.
Wichtig sind die Importe – by braucht die Delegat-Erweiterung:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedStateWo mir das im Alltag begegnet
Der Splash-Screen ist nur das Lehrbuchbeispiel. In echten Apps trifft es mich vor allem hier:
@Composable
fun AutoSave(text: String, onSave: (String) -> Unit) {
val currentOnSave by rememberUpdatedState(onSave)
val currentText by rememberUpdatedState(text)
LaunchedEffect(Unit) { // eine Schleife für die gesamte Lebenszeit
while (true) {
delay(30_000)
currentOnSave(currentText) // speichert IMMER den aktuellen Stand
}
}
}Ohne rememberUpdatedState würde dieses Auto-Save alle 30 Sekunden brav den Text speichern, den der Nutzer beim Öffnen des Bildschirms hatte. Der Fehler fällt im Test nicht auf, weil man beim Prüfen ja meist nichts ändert – und im Alltag schon.
Dasselbe Muster gilt für DisposableEffect: Auch ein Listener, den du nur einmal registrierst, ruft sonst dauerhaft das eingefrorene Lambda auf.
Wann du es NICHT brauchst
- Wenn der Effekt ohnehin bei jeder Änderung neu starten soll – dann gehört der Wert schlicht in den Schlüssel:
LaunchedEffect(userId) { … }. - Für
State,FlowoderViewModel-Referenzen, die du direkt im Effekt liest. Die sind selbst schon beobachtbar oder stabil. - Für einfache Werte, die sich nie ändern.
Die Faustregel, die ich mir gemerkt habe: Sobald in einem Effekt mit fixem Schlüssel ein Lambda oder ein veränderlicher Parameter von außen aufgerufen wird, gehört ein rememberUpdatedState davor.
Wenn du eine Android-App planst, bei der solche Details von Anfang an sitzen statt später mühsam gesucht zu werden: schreib mir einfach.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
animateAsState in Jetpack Compose – flüssige Animationen in einer Zeile
Du willst, dass sich Größe, Farbe oder Position weich verändern, statt hart umzuspringen? In Compose brauchst du dafür keinen Animator und keinen State-Maschinen-Zoo – ein animateDpAsState oder animateColorAsState reicht. Ich zeige dir, wie das funktioniert.
Sequences in Kotlin – große Datenmengen faul verarbeiten und früh aufhören
Verkettete map/filter bauen bei jedem Schritt eine komplette Zwischenliste. Mit asSequence() läuft die Kette element-für-element – und hört auf, sobald genug Treffer da sind. Wann sich das lohnt und wann nicht.
value class in Kotlin – typsichere IDs ohne Extra-Kosten zur Laufzeit
Ein `String` für die Nutzer-ID, ein `String` für die Bestell-ID – und schon vertauschst du sie irgendwann. Mit `@JvmInline value class` bekommst du echte, unverwechselbare Typen, die zur Laufzeit trotzdem nur der nackte Wert sind. Kein Wrapper-Objekt, keine Bremse.