stateIn und shareIn – aus einem kalten Flow wird geteilter Zustand
Jeder Sammler eines kalten Flows startet die Arbeit neu – zwei Beobachter, zwei Datenbankabfragen. stateIn und shareIn machen daraus einen Strom, den sich alle teilen, samt aktuellem Wert und Abschalten bei Inaktivität.
Ein Flow aus Room oder DataStore ist kalt: Er tut nichts, bis jemand sammelt – und tut es für jeden Sammler von vorn. Zwei Stellen in der Oberfläche, die denselben Flow beobachten, lösen also zwei Abfragen aus. Nach einer Drehung kommt eine dritte dazu.
stateIn: ein Wert, der immer da ist
class ArtikelViewModel(repo: Repo) : ViewModel() {
val artikel: StateFlow<List<Artikel>> = repo.beobachteArtikel() // kalter Flow
.map { liste -> liste.sortedBy(Artikel::name) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
}Drei Dinge passieren hier:
- Die Kette läuft einmal, egal wie viele sammeln.
- Jeder neue Sammler bekommt sofort den letzten Wert (
value) – kein leerer Bildschirm nach der Drehung. WhileSubscribed(5_000)hält den Strom noch fünf Sekunden nach dem letzten Abmelden am Leben. Genau das überbrückt eine Bildschirmdrehung, ohne bei einem echten Verlassen weiterzulaufen.
Die drei Startstrategien
SharingStarted.Eagerly– läuft sofort und bis zum Ende des Scopes. Für Dinge, die immer aktuell sein müssen (Anmeldestatus).SharingStarted.Lazily– startet beim ersten Sammler und hört nie wieder auf.SharingStarted.WhileSubscribed(stopTimeoutMillis)– der Normalfall in Android.
Die 5 Sekunden sind kein Aberglaube: Sie sind lang genug für eine Konfigurationsänderung und kurz genug, um beim Verlassen des Bildschirms wirklich abzuschalten.
shareIn: wenn es keinen „aktuellen Wert" gibt
val meldungen: SharedFlow<Meldung> = socket.beobachte()
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
replay = 0, // wer später kommt, sieht alte Meldungen nicht
)Faustregel: Zustand mit stateIn (Liste, Formular, Ladezustand – es gibt immer ein „jetzt gerade"), Ereignisse mit shareIn (Push-Nachricht, Toast, Navigationsbefehl – die sind vorbei, wenn sie vorbei sind).
In Compose sammeln
@Composable
fun ArtikelListe(vm: ArtikelViewModel) {
val liste by vm.artikel.collectAsStateWithLifecycle()
LazyColumn { items(liste, key = { it.id }) { ArtikelZeile(it) } }
}collectAsStateWithLifecycle() meldet sich ab, wenn der Bildschirm in den Hintergrund geht – zusammen mit WhileSubscribed hört dann auch die Datenquelle auf zu arbeiten.
Fallstrick
Zwei Punkte. Erstens: stateIn gibt es auch in einer suspendierenden Variante ohne initialValue – die wartet auf den ersten Wert und blockiert damit den Aufrufer; in einem ViewModel-Feld willst du fast immer die Fassung mit Startwert. Zweitens: StateFlow lässt gleiche Werte aus (distinctUntilChanged ist eingebaut). Modellier deinen Zustand deshalb als data class mit sinnvollem equals – sonst wunderst du dich, warum ein erneutes Laden „nichts tut", obwohl sich intern etwas geändert hat.
Wenn deine App bei jeder Drehung neu lädt oder Daten doppelt holt, ist das meistens hier begründet – ich schaue gern drauf: bymw.de.
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
viewModelScope – Coroutinen, die mit dem Bildschirm verschwinden
Eine Coroutine, die nach dem Schließen des Bildschirms weiterläuft, schreibt in ein ViewModel, das niemand mehr sieht – und hält im schlimmsten Fall die ganze Activity im Speicher. viewModelScope beendet sie automatisch.
Room-Migrationen – das Schema ändern, ohne Nutzerdaten zu verlieren
Eine neue Spalte in der Entity, und beim nächsten Start ist der Spielstand weg – weil fallbackToDestructiveMigration die Datenbank einfach löscht. So machst du es richtig, inklusive Test.
Fehler in Coroutinen – coroutineScope, supervisorScope und wer wen mitreißt
Eine von fünf parallelen Abfragen scheitert – und plötzlich sind alle abgebrochen. Das ist kein Bug, sondern strukturierte Nebenläufigkeit. Wer den Unterschied zwischen coroutineScope und supervisorScope kennt, steuert das bewusst.