MWCodebymw.de ↗
Kotlin

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.

Wer in Android Coroutinen selbst startet, muss sie auch selbst beenden:

// So nicht: der Scope überlebt das ViewModel
GlobalScope.launch { repo.lade() }

GlobalScope läuft bis zum Ende des Prozesses. Dreht der Nutzer das Gerät oder geht zurück, arbeitet die Coroutine weiter, hält Referenzen fest und schreibt Ergebnisse in ein ViewModel, das es fachlich nicht mehr gibt.

Der eingebaute Scope

class BestellungenViewModel(private val repo: Repo) : ViewModel() {

    private val _stand = MutableStateFlow<Zustand>(Zustand.Laedt)
    val stand: StateFlow<Zustand> = _stand.asStateFlow()

    fun laden() {
        viewModelScope.launch {
            _stand.value = try {
                Zustand.Fertig(repo.lade())          // suspend-Funktion
            } catch (e: IOException) {
                Zustand.Fehler("Keine Verbindung")
            }
        }
    }
}

viewModelScope ist eine Erweiterung aus androidx.lifecycle:lifecycle-viewmodel-ktx. Er wird in onCleared() automatisch abgebrochen – also genau dann, wenn das ViewModel endgültig verschwindet, nicht schon bei einer Drehung.

Er läuft auf dem Hauptfaden – und das ist Absicht

viewModelScope benutzt Dispatchers.Main.immediate. Zustandsänderungen landen damit ohne Umweg auf dem UI-Faden. Die eigentliche Arbeit gehört trotzdem woandershin – aber in die Funktion, nicht in den Aufruf:

class Repo(private val api: Api, private val dao: Dao) {
    suspend fun lade(): List<Bestellung> = withContext(Dispatchers.IO) {
        val roh = api.hole()
        dao.speichern(roh)
        roh.map { it.toModell() }
    }
}

Diese Regel („suspend-Funktionen sind aufrufsicher") nimmt dem Aufrufer die Frage nach dem richtigen Dispatcher ab.

Doppelte Klicks und Neuladen abfangen

private var job: Job? = null

fun neuLaden() {
    job?.cancel()                    // alte Abfrage verwerfen
    job = viewModelScope.launch { … }
}

Ohne das rennen zwei Abfragen gegeneinander, und wer zuletzt antwortet, gewinnt – ein Fehler, der sich nur bei langsamer Verbindung zeigt.

Abbruch ist kooperativ

viewModelScope.launch {
    for (datei in dateien) {
        ensureActive()               // prüft, ob abgebrochen wurde
        verarbeite(datei)
    }
}

Eine reine Rechenschleife merkt von einem cancel() nichts. ensureActive() (oder ein yield()) baut die Prüfstelle ein. Und ganz wichtig: try/catch um alles herum darf CancellationException nicht schlucken – fang lieber gezielt die Fehler, mit denen du rechnest.

Fallstrick

Arbeit, die weiterlaufen muss, gehört nicht in viewModelScope: Ein Upload, der beim Zurückgehen abgebrochen wird, ist kein Feature. Dafür gibt es WorkManager oder einen Scope in der Anwendungsschicht. Umgekehrt gilt: Alles, was nur den aktuellen Bildschirm betrifft, gehört genau hierhin – und nicht in einen Scope, der den Rest des Prozesses überlebt.

Android-Entwicklung mit Kotlin, Compose und sauberem Lebenszyklus ist mein Alltag. Melde dich über bymw.de.

Quellen

#Kotlin#Coroutinen#ViewModel#Android#Lebenszyklus

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 →