Kotlin-Koroutinen unter Android

Eine Koroutine ist ein Designmuster für Nebenläufigkeit, mit dem Sie in Android asynchron ausgeführten Code vereinfachen können. Coroutinen wurden Kotlin in Version 1.3 hinzugefügt und basieren auf etablierten Konzepten aus anderen Sprachen.

Unter Android helfen Koroutinen bei der Verwaltung von Aufgaben mit langer Ausführungszeit, die andernfalls den Hauptthread blockieren und dazu führen können, dass Ihre App nicht mehr reagiert. Über 50% der professionellen Entwickler, die Koroutinen verwenden, haben gemeldet, dass sie ihre Produktivität gesteigert haben. In diesem Thema wird beschrieben, wie Sie diese Probleme mit Kotlin-Coroutinen lösen und damit saubereren und prägnanteren App-Code schreiben können.

Funktionen

Coroutines ist unsere empfohlene Lösung für die asynchrone Programmierung unter Android. Hier einige Beispiele:

  • Einfach: Sie können viele Koroutinen für einen einzelnen Thread ausführen, da die Sperrung unterstützt wird, die den Thread, in dem die Koroutine ausgeführt wird, nicht blockiert. Durch das Anhalten wird Arbeitsspeicher übermäßig blockiert und es werden viele gleichzeitige Vorgänge unterstützt.
  • Weniger Speicherlecks: Verwenden Sie strukturierte Nebenläufigkeit, um Vorgänge innerhalb eines Bereichs auszuführen.
  • Integrierte Unterstützung für Stornierungen: Stornierungen werden automatisch über die Hierarchie der laufenden Koroutinen weitergegeben.
  • Jetpack-Integration: Viele Jetpack-Bibliotheken enthalten Erweiterungen, die vollständige Unterstützung für Koroutinen bieten. Einige Bibliotheken bieten auch einen eigenen Koroutinebereich, den Sie für strukturierte Nebenläufigkeit verwenden können.

Beispiele – Übersicht

Basierend auf dem Leitfaden zur Anwendungsarchitektur wird in den Beispielen in diesem Thema eine Netzwerkanfrage gestellt und das Ergebnis an den Hauptthread zurückgegeben, damit die Anwendung dem Nutzer das Ergebnis anzeigen kann.

Insbesondere ruft die Architekturkomponente ViewModel die Repository-Ebene im Hauptthread auf, um die Netzwerkanfrage auszulösen. In dieser Anleitung werden verschiedene Lösungen durchgegangen, die Koroutinen verwenden, damit der Hauptthread nicht blockiert wird.

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

Informationen zu Abhängigkeiten

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

Groovy

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

Kotlin

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

In einem Hintergrundthread ausführen

Wenn eine Netzwerkanfrage an den Hauptthread gesendet wird, wird dieser gewartet bzw. blockieren, bis er eine Antwort erhält. Da der Thread blockiert ist, kann das Betriebssystem onDraw() nicht aufrufen. Dies führt dazu, dass die Anwendung einfriert und möglicherweise ein Dialogfeld „Anwendung antwortet nicht“ (ANR) angezeigt wird. Für eine bessere Nutzererfahrung führen wir diesen Vorgang in einem Hintergrundthread aus.

Sehen wir uns zuerst die Klasse Repository an und sehen wir uns an, wie sie die Netzwerkanfrage sendet:

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

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // 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 auf die Netzwerkanfrage zu modellieren, verwenden wir eine eigene Result-Klasse.

Der ViewModel löst die Netzwerkanfrage aus, wenn der Nutzer beispielsweise auf eine Schaltfläche klickt:

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

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

Beim vorherigen Code blockiert LoginViewModel den UI-Thread beim Senden der Netzwerkanfrage. Die einfachste Lösung, um die Ausführung aus dem Hauptthread zu verschieben, besteht darin, eine neue Koroutine 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 Koroutinencode in der Funktion login an:

  • viewModelScope ist ein vordefinierter CoroutineScope-Wert, der in den ViewModel-KTX-Erweiterungen enthalten ist. Alle Koroutinen müssen in einem Bereich ausgeführt werden. Ein CoroutineScope verwaltet eine oder mehrere zugehörige Koroutinen.
  • launch ist eine Funktion, die eine Koroutine erstellt und die Ausführung ihres Funktionskörpers an den entsprechenden Disponenten sendet.
  • Dispatchers.IO gibt an, dass diese Koroutine 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 aus der View-Ebene im Hauptthread auf.
  • launch erstellt eine neue Koroutine und die Netzwerkanfrage wird unabhängig von einem Thread gestellt, der für E/A-Vorgänge reserviert ist.
  • Während die Koroutine ausgeführt wird, setzt die Funktion login die Ausführung fort und wird zurückgegeben, möglicherweise bevor die Netzwerkanfrage abgeschlossen ist. Der Einfachheit halber wird die Netzwerkantwort vorerst ignoriert.

Da diese Koroutine mit viewModelScope gestartet wird, wird sie im Bereich von ViewModel ausgeführt. Wenn ViewModel gelöscht wird, weil der Nutzer den Bildschirm verlässt, wird viewModelScope automatisch abgebrochen und alle ausgeführten Koroutinen werden ebenfalls abgebrochen.

Ein Problem beim vorherigen Beispiel besteht darin, dass beim Aufrufen von makeLoginRequest daran gemerkt werden muss, die Ausführung explizit aus dem Hauptthread zu verschieben. Sehen wir uns an, wie wir Repository ändern können, um dieses Problem zu lösen.

Koroutinen für „main_safety“ verwenden

Eine Funktion gilt als hauptsicher, wenn sie keine UI-Aktualisierungen im Hauptthread blockiert. Die Funktion makeLoginRequest ist nicht hauptsicher, da der Aufruf von makeLoginRequest aus dem Hauptthread die UI blockiert. Verwenden Sie die Funktion withContext() aus der Coroutinen-Bibliothek, um die Ausführung einer Koroutine in einen anderen Thread zu 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
        }
    }
}

withContext(Dispatchers.IO) verschiebt die Ausführung der Koroutine in einen E/A-Thread. Dadurch ist unsere aufrufende Funktion stabil und die UI kann nach Bedarf aktualisiert werden.

makeLoginRequest ist auch mit dem Suchbegriff suspend gekennzeichnet. Mit diesem Schlüsselwort wird in Kotlin der Aufruf einer Funktion innerhalb einer Koroutine erzwungen.

Im folgenden Beispiel wird die Koroutine im LoginViewModel erstellt. Da makeLoginRequest die Ausführung aus dem Hauptthread verschiebt, kann die Koroutine 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 wird auch hier benötigt, 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 verwendet keinen Dispatchers.IO-Parameter. Wenn Sie Dispatcher nicht an launch übergeben, werden alle von viewModelScope gestarteten Koroutinen im Hauptthread ausgeführt.
  • Das Ergebnis der Netzwerkanfrage wird nun verarbeitet, um die UI für Erfolg oder Fehlschlag anzuzeigen.

Die Anmeldefunktion wird nun wie folgt ausgeführt:

  • Die App ruft die Funktion login() aus der View-Ebene im Hauptthread auf.
  • launch erstellt eine neue Koroutine im Hauptthread und beginnt mit der Ausführung.
  • Innerhalb der Koroutine wird durch den Aufruf von loginRepository.makeLoginRequest() jetzt die weitere Ausführung der Koroutine gesperrt, bis der Block withContext in makeLoginRequest() beendet ist.
  • Sobald der Block withContext abgeschlossen ist, setzt die Koroutine in login() die Ausführung im Hauptthread mit dem Ergebnis der Netzwerkanfrage fort.

Umgang mit Ausnahmen

Verwenden Sie die integrierte Unterstützung von Kotlin für Ausnahmen, 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, in der UI als Fehler behandelt.

Zusätzliche Ressourcen für Koroutinen

Ausführlichere Informationen zu Koroutinen unter Android finden Sie unter App-Leistung mit Kotlin-Koroutinen verbessern.

Weitere Ressourcen zu Koroutinen finden Sie unter den folgenden Links: