Android Coden
Android 4 min lesen

StateFlow in ViewModels

StateFlow verbindet dein ViewModel sicher mit der Compose-UI. Lerne, wie du reaktive Zustandsstreams lifecycle-bewusst aussetzt.

Reactive UI-Programmierung in Android bedeutet: Die UI beschreibt, wie sie bei einem bestimmten Zustand aussieht – und nicht, was sie bei jedem Event tun soll. StateFlow ist das Werkzeug, das diesen Zustand aus dem ViewModel heraus sauber nach oben in die Compose-UI trägt, ohne dabei den Lifecycle zu vergessen.

Was ist das?

StateFlow ist ein spezieller reaktiver Datenstrom aus der Kotlin-Coroutines-Bibliothek. Er besitzt immer genau einen aktuellen Wert – anders als ein einfacher Flow, der zwischen zwei Emissionen keinen Zustand kennt. Dieser Wert ist niemals null und wird sofort an neue Subscriber ausgegeben, sobald diese anfangen zu collecten.

Im Android-Architektur-Kontext lebt StateFlow im ViewModel. Das ViewModel hält die fachliche Logik und den UI-Zustand, weiß aber nichts von Activities, Fragments oder Composables. StateFlow ist die Brücke: Das ViewModel schreibt Zustandsänderungen in den Flow, die UI liest daraus – ohne dass beide direkt miteinander gekoppelt sind. Das ist kein Luxus, sondern das Fundament für testbaren, stabilen Code.

StateFlow löst ein konkretes Problem: Vor der Coroutines-Ära nutzte man LiveData, um UI-Zustand lifecycle-sicher zu observieren. LiveData ist aber eng an den Android-Lifecycle gebunden und außerhalb von Android-Komponenten kaum testbar. StateFlow stammt aus reinem Kotlin und funktioniert in Unit-Tests ohne Android-Infrastruktur.

Wie funktioniert es?

StateFlow besteht intern aus zwei Typen: MutableStateFlow zum Schreiben und StateFlow als read-only Interface zum Lesen. Im ViewModel erstellst du einen MutableStateFlow mit einem Startwert und gibst ihn nach außen als StateFlow frei:

class CounterViewModel : ViewModel() {

    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() {
        _count.value++
    }
}

Das Muster private val _state / val state ist in der Android-Welt Konvention. Die UI darf nur lesen; schreiben darf nur das ViewModel selbst.

Wann wird ein neuer Wert emittiert?

StateFlow prüft Gleichheit per equals(). Wenn du _count.value = 0 setzt und der aktuelle Wert bereits 0 ist, passiert nichts – kein Recompose, kein Update. Das ist ein wichtiges Verhalten, wenn du komplexe Datenobjekte als Zustand verwendest: Stelle sicher, dass deine Datenklassen equals() korrekt implementieren, oder nutze data class, die das automatisch tut.

Coroutine-Scope und Update-Muster

Wenn du den Zustand aus einem Coroutine-Kontext heraus änderst – etwa nach einem Netzwerkaufruf –, verwendest du viewModelScope:

fun loadUser(id: String) {
    viewModelScope.launch {
        val user = repository.fetchUser(id)
        _uiState.value = UiState.Success(user)
    }
}

viewModelScope wird automatisch gecancelt, wenn das ViewModel zerstört wird. Du musst dich nicht selbst um Cleanup kümmern.

In der Praxis

In Compose sammelst du StateFlow mit collectAsStateWithLifecycle() aus dem Artifact androidx.lifecycle:lifecycle-runtime-compose. Dieses API pausiert die Collection automatisch, wenn die App in den Hintergrund geht, und schont damit Akku und Ressourcen:

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val count by viewModel.count.collectAsStateWithLifecycle()

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "Zähler: $count", style = MaterialTheme.typography.headlineMedium)
        Button(onClick = { viewModel.increment() }) {
            Text("Erhöhen")
        }
    }
}

Typische Stolperfalle: collectAsState() statt collectAsStateWithLifecycle()

collectAsState() collectet auch dann, wenn die App im Hintergrund ist. In vielen Tutorials wird es noch verwendet, weil es ohne den lifecycle-runtime-compose-Dependency auskommt. Für Produktionscode ist das aber falsch: Die offizielle Empfehlung lautet, immer collectAsStateWithLifecycle() zu verwenden. Es ist ein kleiner Unterschied im Namen, aber ein großer im Ressourcenverbrauch.

UI-Zustand als versiegelte Klasse modellieren

Statt primitive Typen in StateFlow zu packen, modellierst du den vollständigen UI-Zustand als sealed class oder data class:

sealed class UiState {
    object Loading : UiState()
    data class Success(val items: List<String>) : UiState()
    data class Error(val message: String) : UiState()
}

private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

In Compose kannst du dann mit einem when-Ausdruck auf den Zustand reagieren und jeden Fall explizit behandeln. Der Compiler warnt dich, wenn du einen Fall vergisst.

Fazit

StateFlow im ViewModel ist kein optionales Upgrade, sondern der aktuelle Standard für reaktives UI-Zustandsmanagement in modernem Android. Die Kombination aus MutableStateFlow im ViewModel und collectAsStateWithLifecycle() in Compose sorgt dafür, dass deine UI immer den aktuellen Zustand zeigt, ohne Lifecycle-Probleme zu riskieren. Überprüfe in deinen eigenen Projekten: Verwendest du noch LiveData oder collectAsState()? Migriere einen Screen auf StateFlow mit collectAsStateWithLifecycle(), schreibe einen einfachen ViewModel-Unit-Test, der den emittierten Zustand prüft, und beobachte im Debugger, wie Recomposes nur bei echten Zustandsänderungen ausgelöst werden.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.