Kotlin-Koroutinen unter Android

Eine Coroutine ist ein Muster für die Nebenläufigkeit, mit dem Sie Code, der asynchron ausgeführt wird, in Android vereinfachen können. Coroutinen wurden in Kotlin 1.3 eingeführt und basieren auf etablierten Konzepten aus anderen Sprachen.

Unter Android helfen Coroutinen dabei, lang andauernde Aufgaben zu verwalten, die ansonsten den Hauptthread blockieren und dazu führen könnten, dass Ihre App nicht mehr reagiert. Über 50% der professionellen Entwickler, die Coroutinen verwenden, haben eine höhere Produktivität gemeldet. In diesem Thema wird beschrieben, wie Sie diese Probleme mit Kotlin-Coroutinen beheben und so saubereren und prägnanteren App-Code schreiben können.

Features

Coroutines sind unsere empfohlene Lösung für die asynchrone Programmierung auf Android. Zu den wichtigsten Funktionen gehören:

  • Ressourcenschonend: Dank der Unterstützung für Unterbrechung können Sie viele Coroutinen in einem einzelnen Thread ausführen, ohne den Thread zu blockieren, in dem die Coroutine ausgeführt wird. Durch das Anhalten wird im Vergleich zum Blockieren Arbeitsspeicher gespart und es werden viele gleichzeitige Vorgänge unterstützt.
  • Weniger Speicherlecks: Verwenden Sie strukturierte Parallelität, um Vorgänge in einem Bereich auszuführen.
  • Integrierte Unterstützung für den Abbruch: Abbruch wird automatisch über die laufende Coroutine-Hierarchie weitergegeben.
  • Jetpack-Integration: Viele Jetpack-Bibliotheken enthalten Erweiterungen, die vollständige Coroutine-Unterstützung bieten. Einige Bibliotheken stellen auch einen eigenen Coroutine-Scope bereit, den Sie für strukturierte Nebenläufigkeit verwenden können.

Beispiele – Übersicht

Gemäß der Anleitung zur App-Architektur wird in den Beispielen in diesem Thema eine Netzwerkanfrage gestellt und das Ergebnis an den Hauptthread zurückgegeben, wo die App das Ergebnis dann dem Nutzer präsentieren kann.

Genauer gesagt ruft die Architekturkomponente ViewModel die Repository-Ebene im Hauptthread auf, um die Netzwerkanfrage auszulösen. In diesem Leitfaden werden verschiedene Lösungen durchlaufen, bei denen der Hauptthread mithilfe von Coroutinen nicht blockiert wird.

ViewModel enthält eine Reihe von KTX-Erweiterungen, die direkt mit Coroutinen funktionieren. Diese Erweiterungen sind Teil der lifecycle-viewmodel-ktx-Bibliothek und werden in dieser Anleitung verwendet.

Abhängigkeitsinformationen

Wenn Sie Coroutinen in Ihrem Android-Projekt verwenden möchten, fügen Sie der Datei build.gradle Ihrer App die folgende Abhängigkeit hinzu:

Cool

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Ausführung in einem Hintergrundthread

Wenn Sie eine Netzwerkanfrage im Hauptthread stellen, wartet dieser oder wird blockiert, bis er eine Antwort erhält. Da der Thread blockiert ist, kann das Betriebssystem onDraw() nicht aufrufen. Dies führt dazu, dass Ihre App einfriert und möglicherweise ein ANR-Dialogfeld (Application Not Responding) angezeigt wird. Um die Nutzerfreundlichkeit zu verbessern, führen wir diesen Vorgang in einem Hintergrundthread aus.

Sehen wir uns zuerst die Klasse Repository an und wie sie die Netzwerkanfrage stellt:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

private const val loginUrl = "https://example.com/login"

class LoginRepository(private val responseParser: LoginResponseParser) {
    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

makeLoginRequest ist synchron und blockiert den aufrufenden Thread. Um die Antwort der Netzwerkanfrage zu modellieren, haben wir eine eigene Result-Klasse.

Die ViewModel löst die Netzwerkanfrage aus, wenn der Nutzer beispielsweise auf einen Button klickt:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Im vorherigen Code blockiert LoginViewModel den UI-Thread, wenn die Netzwerkanfrage gesendet wird. Die einfachste Lösung, die Ausführung vom Hauptthread zu verlagern, besteht darin, eine neue Coroutine zu erstellen und die Netzwerkanfrage in einem E/A-Thread auszuführen:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

Sehen wir uns den Coroutinen-Code in der Funktion login an:

  • viewModelScope ist eine vordefinierte CoroutineScope, die in den ViewModel-KTX-Erweiterungen enthalten ist. Alle Coroutinen müssen in einem Bereich ausgeführt werden. Ein CoroutineScope verwaltet eine oder mehrere zugehörige Coroutinen.
  • launch ist eine Funktion, die eine Coroutine erstellt und die Ausführung ihres Funktionsrumpfs an den entsprechenden Dispatcher weiterleitet.
  • Dispatchers.IO gibt an, dass diese Coroutine in einem Thread ausgeführt werden soll, der für E/A-Vorgänge reserviert ist.

Die Funktion login wird so ausgeführt:

  • Die App ruft die Funktion login über die Ebene View im Hauptthread auf.
  • Mit launch wird eine neue Coroutine erstellt und die Netzwerkanfrage wird unabhängig in einem Thread ausgeführt, der für E/A-Vorgänge reserviert ist.
  • Während die Coroutine ausgeführt wird, wird die Ausführung der Funktion login fortgesetzt und sie wird zurückgegeben, möglicherweise bevor die Netzwerkanfrage abgeschlossen ist. Der Einfachheit halber wird die Netzwerkantwort vorerst ignoriert.

Da diese Coroutine mit viewModelScope gestartet wird, wird sie im Bereich von ViewModel ausgeführt. Wenn ViewModel zerstört wird, weil der Nutzer den Bildschirm verlässt, wird viewModelScope automatisch abgebrochen und alle laufenden Coroutinen werden ebenfalls abgebrochen.

Ein Problem mit dem vorherigen Beispiel besteht darin, dass bei jedem Aufruf von makeLoginRequest die Ausführung explizit vom Hauptthread weg verlagert werden muss. Sehen wir uns an, wie wir die Repository ändern können, um dieses Problem zu beheben.

Coroutinen für Main-Safety verwenden

Eine Funktion gilt als main-safe, wenn sie keine UI-Aktualisierungen im Hauptthread blockiert. Die Funktion makeLoginRequest ist nicht hauptthread-sicher, da der Aufruf von makeLoginRequest über den Hauptthread die Benutzeroberfläche blockiert. Mit der Funktion withContext() aus der Coroutines-Bibliothek können Sie die Ausführung einer Coroutine in einen anderen Thread verschieben:

class LoginRepository(
    // ...
) {
    // ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

Mit withContext(Dispatchers.IO) wird die Ausführung der Coroutine in einen I/O-Thread verschoben. Dadurch wird unsere aufrufende Funktion „main-safe“ und die Benutzeroberfläche kann bei Bedarf aktualisiert werden.

makeLoginRequest ist auch mit dem Keyword suspend gekennzeichnet. Mit diesem Schlüsselwort wird in Kotlin erzwungen, dass eine Funktion aus einer Coroutine aufgerufen wird.

Im folgenden Beispiel wird die Coroutine in der LoginViewModel erstellt. Da makeLoginRequest die Ausführung aus dem Hauptthread verschiebt, kann die Coroutine in der Funktion login jetzt im Hauptthread ausgeführt werden:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> { /* Happy path */ }
                else -> { /* Show error in UI */ }
            }
        }
    }
}

Die Koroutine ist hier weiterhin erforderlich, da makeLoginRequest eine suspend-Funktion ist und alle suspend-Funktionen in einer Koroutine ausgeführt werden müssen.

Dieser Code unterscheidet sich in einigen Punkten vom vorherigen login-Beispiel:

  • launch akzeptiert keinen Dispatchers.IO-Parameter. Wenn Sie kein Dispatcher an launch übergeben, werden alle von viewModelScope gestarteten Koroutinen im Hauptthread ausgeführt.
  • Das Ergebnis der Netzwerkanfrage wird jetzt verarbeitet, um die Benutzeroberfläche für Erfolg oder Fehler anzuzeigen.

Die Anmeldefunktion wird jetzt so ausgeführt:

  • Die App ruft die Funktion login() über die Ebene View im Hauptthread auf.
  • launch erstellt eine neue Coroutine im Hauptthread und die Ausführung der Coroutine beginnt.
  • Innerhalb der Coroutine wird durch den Aufruf von loginRepository.makeLoginRequest() die weitere Ausführung der Coroutine angehalten, bis der withContext-Block in makeLoginRequest() abgeschlossen ist.
  • Wenn der withContext-Block abgeschlossen ist, wird die Coroutine in login() im Hauptthread mit dem Ergebnis der Netzwerkanfrage fortgesetzt.

Umgang mit Ausnahmen

Verwenden Sie die integrierte Unterstützung für Ausnahmen in Kotlin, um Ausnahmen zu verarbeiten, die von der Repository-Ebene ausgelöst werden können. Im folgenden Beispiel verwenden wir einen try-catch-Block:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch (e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> { /* Happy path */ }
                else -> { /* Show error in UI */ }
            }
        }
    }
}

In diesem Beispiel wird jede unerwartete Ausnahme, die vom makeLoginRequest()-Aufruf ausgelöst wird, als Fehler in der Benutzeroberfläche behandelt.

Zusätzliche Ressourcen zu Coroutinen

Weitere Informationen zu Coroutinen in Android

Weitere Informationen zu Coroutines finden Sie unter den folgenden Links: