Kotlin Coroutines: Asynchrone Android-Programmierung verstehen

Jede ernsthafte Android-App muss mit asynchronen Aufgaben umgehen: Netzwerkanfragen, Datenbankzugriffe, das Laden von Bildern oder das Verarbeiten großer Datenmengen. Würden diese Operationen direkt im Haupt-Thread laufen, würde die App einfrieren und im schlimmsten Fall mit einem „Application Not Responding"-Dialog abstürzen. Kotlin Coroutines sind heute der Standardweg, um solche Aufgaben sauber und lesbar im Hintergrund auszuführen – ohne das berüchtigte Callback-Chaos früherer Lösungen.

Warum nicht einfach Threads oder Callbacks?

Klassisch hat man auf Android mit Thread, AsyncTask (inzwischen veraltet), Handler oder Bibliotheken wie RxJava gearbeitet. Diese Ansätze funktionieren, haben aber Nachteile: Threads sind teuer und schwer zu koordinieren, Callbacks verschachteln sich bei mehreren abhängigen Operationen zur sogenannten Callback-Hölle, und die Fehlerbehandlung wird schnell unübersichtlich.

Coroutines lösen das, indem sie asynchronen Code so schreiben lassen, als wäre er sequenziell. Eine Netzwerkanfrage gefolgt von einer Datenbankspeicherung liest sich Zeile für Zeile von oben nach unten – obwohl im Hintergrund gewartet, der Thread freigegeben und später fortgesetzt wird.

suspend-Funktionen: Das Fundament

Der Kern von Coroutines ist das Schlüsselwort suspend. Eine als suspend markierte Funktion kann ihre Ausführung pausieren und später fortsetzen, ohne den darunterliegenden Thread zu blockieren. Während sie wartet (etwa auf eine Server-Antwort), gibt sie den Thread frei, der dann andere Arbeit erledigen kann.

Der entscheidende Punkt: suspend macht eine Funktion nicht automatisch nebenläufig. Es markiert nur, dass sie pausieren darf. Die eigentliche Nebenläufigkeit entsteht durch Coroutine-Builder und Dispatcher.

Dispatcher: Auf welchem Thread läuft was?

Ein Dispatcher bestimmt, auf welchem Thread oder Thread-Pool eine Coroutine ausgeführt wird. Die Standard-Bibliothek liefert drei wichtige Varianten:

Den Wechsel zwischen Dispatchern erledigst du elegant mit withContext(Dispatchers.IO) { ... }. Innerhalb dieses Blocks läuft der Code im IO-Pool, danach kehrt die Coroutine automatisch zum vorherigen Kontext zurück. So holst du Daten im Hintergrund und aktualisierst danach gefahrlos die UI auf dem Main-Thread – alles in einer einzigen, gut lesbaren Funktion.

Structured Concurrency: Coroutines, die nicht entwischen

Ein konzeptionell wichtiges Prinzip ist die structured concurrency (strukturierte Nebenläufigkeit). Jede Coroutine lebt innerhalb eines CoroutineScope. Wird der Scope abgebrochen, werden automatisch alle darin gestarteten Coroutines mit abgebrochen. Das verhindert sogenannte „verwaiste" Coroutines, die im Hintergrund weiterlaufen, obwohl ihr Bildschirm längst geschlossen ist – eine klassische Quelle für Speicherlecks und Abstürze.

Vorgefertigte Scopes in Android

Diese mitgelieferten Scopes nehmen dir die Lebenszyklus-Verwaltung weitgehend ab. In der Praxis startest du im ViewModel eine Coroutine mit viewModelScope.launch { ... } und musst dich um das Aufräumen nicht mehr kümmern.

launch und async: Die Coroutine-Builder

Zwei Builder starten Coroutines, mit einem wichtigen Unterschied:

async glänzt bei parallelen Aufgaben: Startest du zwei unabhängige Netzwerkanfragen jeweils mit async und rufst danach beide await() auf, laufen sie nebeneinander statt nacheinander. Bei zwei Anfragen von je einer Sekunde wartest du so insgesamt etwa eine Sekunde statt zwei – ein spürbarer Geschwindigkeitsgewinn.

Fehlerbehandlung und Abbruch

Coroutines integrieren sich nahtlos in Kotlins normales Exception-Handling. Eine fehlgeschlagene Netzwerkanfrage fängst du einfach mit einem klassischen try/catch-Block ab – kein separater Error-Callback nötig. Für übergeordnete Fehlerbehandlung gibt es den CoroutineExceptionHandler.

Genauso wichtig ist Cancellation. Coroutines sind kooperativ abbrechbar: Sie prüfen an Suspendierungspunkten, ob ein Abbruch angefordert wurde. Lange CPU-Schleifen ohne Suspendierungspunkt sollten daher gelegentlich ensureActive() oder yield() aufrufen, damit ein Abbruch auch tatsächlich greift. Wenn ein Nutzer einen Bildschirm verlässt, werden gebundene Coroutines so sauber gestoppt, statt sinnlos Ressourcen zu verbrauchen.

Flow: Asynchrone Datenströme

Während eine suspend-Funktion genau einen Wert zurückgibt, liefert ein Flow eine Folge von Werten über die Zeit – asynchron und kalt (er startet erst beim Sammeln). Flow ist Kotlins reaktiver Datenstrom und ersetzt in vielen Fällen RxJava-Observables.

Ein Room-Datenbankquery, das einen Flow zurückgibt, sendet automatisch neue Daten, sobald sich die Tabelle ändert. Deine UI reagiert dann reaktiv – ein sehr mächtiges Muster für Apps mit lebendigen, sich ändernden Daten.

Typische Fallstricke

Wer asynchrone Datenmengen visualisieren oder schnell mal JSON-Antworten formatieren will, findet im kotsch.tech Webtools-Bereich hilfreiche Werkzeuge wie einen JSON-Formatter, der beim Debuggen von API-Antworten Zeit spart.

Häufige Fragen

Sind Coroutines dasselbe wie Threads?

Nein. Coroutines laufen auf Threads, sind aber wesentlich leichtgewichtiger. Tausende Coroutines können sich wenige Threads teilen, weil sie beim Warten den Thread freigeben, statt ihn zu blockieren. Coroutines sind also eine Abstraktionsebene über Threads.

Brauche ich noch RxJava?

Für neue Android-Projekte empfiehlt Google Coroutines und Flow. RxJava ist weiterhin mächtig und in Bestandsprojekten verbreitet, aber für die meisten Anwendungsfälle decken Coroutines und Flow den Bedarf mit weniger Komplexität ab.

Was ist der Unterschied zwischen launch und async?

launch ist für Aufgaben ohne Rückgabewert, async für Aufgaben, deren Ergebnis du benötigst und mit await() abholst. Für parallele Berechnungen, deren Ergebnisse du kombinierst, ist async das richtige Werkzeug.

Wie teste ich Code mit Coroutines?

Die Bibliothek kotlinx-coroutines-test bietet runTest und einen Test-Dispatcher, mit dem du Zeit virtuell vorspulen und Coroutine-Code deterministisch testen kannst, ohne echte Wartezeiten.

Funktioniert das auch außerhalb von Android?

Ja. Coroutines sind ein reines Kotlin-Feature und funktionieren auf der JVM, im Backend (etwa mit Ktor), in Kotlin Multiplatform und sogar im Browser via Kotlin/JS. Das gelernte Wissen ist also breit übertragbar.