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
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
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.
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.