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
kotlinx.serialization: JSON und data class ohne Handarbeit verbinden
Eine API antwortet mit JSON, deine App will eine data class. Dazwischen steht in vielen Projekten ein handgeschriebener Parser. Mit kotlinx.serialization sind es eine Annotation und eine Zeile – und ein Schalter, ohne den deine App beim nächsten API-Update abstürzt.
callbackFlow – Listener-APIs in einen Flow verwandeln
Android steckt voller Callback-APIs mit `register`/`unregister`. Mit `callbackFlow` machst du daraus einen ganz normalen Flow – inklusive automatischem Abmelden, wenn niemand mehr zuhört.
AnimatedVisibility in Jetpack Compose – ein- und ausblenden, das nicht springt
Ein Element einfach mit if wegzulassen, poppt hart weg. AnimatedVisibility blendet es weich ein und aus – mit sinnvollen Voreinstellungen und, wenn du willst, deiner eigenen Kombination aus Faden und Bewegung. Ich zeige dir beides.