Asynchrone Programmierung in Kotlin: Vollständiger Leitfaden in der Praxis
Moderne Anwendungen erfordern hohe Leistung und Reaktionsfähigkeit. Asynchrone Programmierung in Kotlin mit Hilfe von Coroutinen (coroutines) ist zum De-facto-Standard für die Arbeit mit Netzwerkanfragen, Datenbanken und Dateioperationen geworden. In diesem Leitfaden werden wir die grundlegenden Konzepte erläutern, praktische Beispiele zeigen und fertige Vorlagen für den Einsatz in Ihren Projekten bereitstellen.
1. Grundlagen der Coroutinen in Kotlin
Coroutinen sind leichtgewichtige Threads, die es ermöglichen, die Ausführung einer Funktion anzuhalten, ohne den Hauptthread zu blockieren. Im Gegensatz zu traditionellen Threads können Coroutinen jederzeit angehalten und später wieder fortgesetzt werden, was sie ideal für asynchrone Operationen macht.
1.1. Einbinden der Abhängigkeiten
Fügen Sie zunächst die Abhängigkeiten in Ihre build.gradle.kts ein:
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") // für Android}1.2. Erste Coroutine
Erstellen wir eine einfache Coroutine mit runBlocking – einer Brücke zwischen der synchronen und der asynchronen Welt:
import kotlinx.coroutines.*
fun main() = runBlocking { launch { delay(1000L) // nicht-blockierende Verzögerung println("Welt!") } println("Hallo,") delay(2000L) // warten auf Beendigung der Coroutine}In diesem Beispiel startet launch eine neue Coroutine, und delay ist eine anhaltende Funktion (suspend), die den Thread nicht blockiert.
2. Suspend-Funktionen und strukturierte Nebenläufigkeit
2.1. Erstellen von Suspend-Funktionen
Jede Funktion, die angehalten werden kann, muss mit dem Modifikator suspend gekennzeichnet werden. Solche Funktionen können nur aus Coroutinen oder anderen Suspend-Funktionen aufgerufen werden:
suspend fun fetchUserData(userId: String): User { delay(2000L) // Simulation einer Netzwerkanfrage return User(userId, "Ivan Petrov")}
suspend fun fetchUserPosts(userId: String): List<Post> { delay(1500L) return listOf(Post("Überschrift 1"), Post("Überschrift 2"))}2.2. Sequenzielle Ausführung
Standardmäßig werden Coroutinen innerhalb eines einzelnen launch sequenziell ausgeführt:
fun main() = runBlocking { val time = measureTimeMillis { val user = fetchUserData("123") val posts = fetchUserPosts("123") println("${user.name} hat ${posts.size} Beiträge") } println("Ausführungszeit: $time ms") // ~3500 ms}2.3. Parallele Ausführung mit async/await
Für die parallele Ausführung verwenden Sie async und await:
fun main() = runBlocking { val time = measureTimeMillis { val user = async { fetchUserData("123") } val posts = async { fetchUserPosts("123") } println("${user.await().name} hat ${posts.await().size} Beiträge") } println("Ausführungszeit: $time ms") // ~2000 ms (parallel)}3. Coroutinen-Dispatcher und Kontexte
Dispatcher legen fest, auf welchem Thread oder Thread-Pool die Coroutine ausgeführt wird. Die wichtigsten Dispatcher:
- Dispatchers.Default – für CPU-intensive Aufgaben (Standard)
- Dispatchers.IO – für Ein-/Ausgabeoperationen (Netzwerk, Dateien)
- Dispatchers.Main – für den UI-Thread (Android, Swing)
- Dispatchers.Unconfined – nicht an einen bestimmten Thread gebunden
3.1. Beispiel mit Dispatchers.IO
fun main() = runBlocking { launch(Dispatchers.IO) { // Netzwerkanfrage val response = httpClient.get("https://api.example.com/data") withContext(Di