MWCodebymw.de ↗
Kotlin

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:

  1. Die Kette läuft einmal, egal wie viele sammeln.
  2. Jeder neue Sammler bekommt sofort den letzten Wert (value) – kein leerer Bildschirm nach der Drehung.
  3. 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

#Kotlin#Flow#StateFlow#Coroutinen#Android

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 →