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.
- Eine
suspend-Funktion kann nur von einer anderensuspend-Funktion oder innerhalb einer Coroutine aufgerufen werden. - Aus Sicht des Aufrufers sieht sie aus wie eine normale Funktion – die Pausier-Mechanik versteckt der Compiler.
- Typische Beispiele aus dem Ökosystem:
delay()stattThread.sleep(), Retrofit-API-Methoden oder Room-Datenbankabfragen.
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:
- Dispatchers.Main – läuft auf dem Haupt-/UI-Thread. Hier aktualisierst du die Oberfläche. Niemals blockierende Arbeit hier ausführen.
- Dispatchers.IO – optimiert für Ein-/Ausgabe wie Netzwerk, Datei- und Datenbankzugriffe. Nutzt einen größeren Thread-Pool, der für wartende Operationen ausgelegt ist.
- Dispatchers.Default – für CPU-intensive Arbeit wie Sortieren, Parsen großer JSON-Strukturen oder Bildverarbeitung. Begrenzt auf die Anzahl der CPU-Kerne.
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
- viewModelScope – an ein ViewModel gebunden. Wird das ViewModel zerstört, enden alle zugehörigen Coroutines. Ideal für Geschäftslogik.
- lifecycleScope – an den Lebenszyklus einer Activity oder eines Fragments gebunden.
- rememberCoroutineScope – innerhalb von Jetpack Compose, um Coroutines aus Event-Callbacks heraus zu starten.
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:
- launch startet eine Coroutine, die kein Ergebnis zurückgibt („fire and forget"). Sie liefert ein
Job-Objekt, mit dem du sie bei Bedarf abbrechen kannst. - async startet eine Coroutine, die ein Ergebnis liefert. Sie gibt ein
Deferredzurück, dessen Wert du später mitawait()abrufst.
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.
- Du sammelst Werte mit
collect { ... }innerhalb einer Coroutine. - Mit Operatoren wie
map,filteroderdebouncetransformierst du den Strom deklarativ. - StateFlow und SharedFlow sind heiße Varianten, die sich hervorragend eignen, um UI-Zustände aus einem ViewModel an die Oberfläche zu liefern – besonders in Kombination mit Jetpack Compose.
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
- GlobalScope vermeiden: Coroutines in
GlobalScopesind an keinen Lebenszyklus gebunden und laufen unkontrolliert weiter. In Produktionscode fast immer ein Fehler. - Blockierende Aufrufe nicht im Main-Dispatcher: Ein
Thread.sleep()oder eine synchrone Datei-Operation blockiert den UI-Thread trotz Coroutine. NutzewithContext(Dispatchers.IO). - runBlocking nur in Tests: Es blockiert den aufrufenden Thread und gehört nicht in Produktions-UI-Code.
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.