Kotlin-Koroutinen unter Android

Eine Koroutine ist ein Designmuster für Gleichzeitigkeit, das Android vereinfacht Code, der asynchron ausgeführt wird. Coroutinen wurden in Version 1.3 zu Kotlin hinzugefügt und basieren auf Konzepten aus anderen Sprachen.

Auf Android können Sie mit gemeinsamen Abläufen lange Aufgaben, die möglicherweise den Hauptthread ansonsten blockieren und dazu führen, dass Ihre App nicht mehr reagiert. Mehr als 50% der professionellen Entwickler, die gemeinsame Routinen verwenden, geben an, gesteigerte Produktivität. In diesem Thema wird beschrieben, wie Sie diese mit Kotlin-Coroutinen angehen. sodass Sie saubereren und präziseren App-Code schreiben können.

Funktionen

Koroutinen ist unsere empfohlene Lösung für die asynchrone Programmierung auf Android Beachten Sie unter anderem Folgendes:

  • Einfach: Sie können mehrere Koroutinen in einem einzelnen Thread ausführen. Unterstützung für Sperrung, Dadurch wird der Thread, in dem die Koroutine ausgeführt wird, nicht blockiert. Wird ausgesetzt Das spart Arbeitsspeicher über die Blockierung und unterstützt gleichzeitig viele gleichzeitige Vorgänge.
  • Weniger Speicherlecks: Verwenden Sie strukturierte Nebenläufigkeit um Vorgänge in einem bestimmten Bereich auszuführen.
  • Integrierte Unterstützung bei Kündigungen: Kündigung wird automatisch durch 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 eigene Coroutine-Bereich, den Sie für strukturierte Nebenläufigkeit.

Beispiele – Übersicht

Die Beispiele basieren auf dem Leitfaden zur App-Architektur. in diesem Thema eine Netzwerkanfrage stellen und das Ergebnis an die Haupt- -Thread, in dem die App dem Nutzer das Ergebnis anzeigen kann.

Konkret gilt: Die ViewModel Die Architekturkomponente ruft die Repository-Ebene im Hauptthread auf, die Netzwerkanfrage auslösen. In diesem Leitfaden werden verschiedene Lösungen die Koroutinen verwenden, um die Blockierung des Hauptthreads aufzuheben.

ViewModel umfasst eine Reihe von KTX-Erweiterungen, die direkt mit Koroutinen. Diese Erweiterungen sind lifecycle-viewmodel-ktx und werden verwendet in diesem Leitfaden.

Informationen zu Abhängigkeiten

Fügen Sie die folgende Abhängigkeit hinzu, um Koroutinen in Ihrem Android-Projekt zu verwenden in die Datei build.gradle Ihrer App ein:

Groovy

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

Kotlin

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

Wird in einem Hintergrundthread ausgeführt

Eine Netzwerkanfrage an den Hauptthread führt dazu, dass dieser wartet bzw. blockiert, bis eine Antwort eingeht. Da der Thread blockiert ist, onDraw() aufrufen. Dies führt dazu, dass deine App einfriert und möglicherweise das Dialogfeld „Application Not Responding (ANR)“ (App antwortet nicht) öffnet. Für bessere Nutzer führen wir diesen Vorgang in einem Hintergrundthread aus.

Sehen wir uns zuerst den Repository-Kurs an. Netzwerkanfrage senden:

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. Zum Modell der Antwort der Netzwerkanfrage haben wir unsere eigene Result-Klasse.

Die ViewModel löst die Netzwerkanfrage aus, wenn der Nutzer klickt, für Beispiel für eine Schaltfläche:

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

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

Mit dem vorherigen Code blockiert LoginViewModel den UI-Thread, wenn die die Netzwerkanfrage stellen. Die einfachste Lösung, um die Ausführung zu verschieben aus dem Hauptthread zu entfernen ist, eine neue Koroutine zu erstellen und das Netzwerk Anfrage zu einem E/A-Thread:

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 Code der Koroutinen in der Funktion login an:

  • viewModelScope ist eine vordefinierte CoroutineScope, die in ViewModel KTX-Erweiterungen. Beachten Sie, dass alle Koroutinen in einem Umfang. Ein CoroutineScope verwaltet eine oder mehrere zugehörige Koroutinen.
  • launch ist eine Funktion, die eine Koroutine erstellt und die Funktion Ausführung seines Funktionskörpers an den entsprechenden Dispatcher.
  • Dispatchers.IO gibt an, dass diese Koroutine in einer Thread ist für E/A-Vorgänge reserviert.

Die Funktion login wird so ausgeführt:

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

Da diese Koroutine mit viewModelScope gestartet wird, wird sie in ausgeführt in den Geltungsbereich des ViewModel. Wenn ViewModel gelöscht wird, weil der verwendet, wird viewModelScope automatisch und alle laufenden Koroutinen werden ebenfalls abgebrochen.

Ein Problem beim vorherigen Beispiel ist, makeLoginRequest muss daran denken, die Ausführung explizit zu verschieben im Hauptthread. Sehen wir uns an, wie wir Repository ändern können, um dieses Problem zu lösen.

Koroutinen für die Hauptsicherheit verwenden

Wir betrachten eine Funktion als main-safe, wenn sie Aktualisierungen der Benutzeroberfläche im im Hauptthread. Die Funktion makeLoginRequest ist nicht hauptsicher, da das Aufrufen von makeLoginRequest aus dem Hauptthread blockiert die UI. Verwenden Sie die Methode Funktion withContext() aus der coroutines-Bibliothek, um die Ausführung zu verschieben einer Koroutine zu einem anderen Thread hinzufügen:

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 eine E/A-Thread, wodurch die aufrufende Funktion hauptsicher wird und die Benutzeroberfläche bei Bedarf aktualisieren.

makeLoginRequest ist auch mit dem Keyword suspend gekennzeichnet. Dieses Keyword ist die Methode von Kotlin, mit der eine Funktion erzwungen wird, die innerhalb einer Koroutine aufgerufen wird.

Im folgenden Beispiel wird die Koroutine in LoginViewModel erstellt. Wenn makeLoginRequest die Ausführung aus dem Hauptthread verschiebt, wird die Koroutine in der Funktion login kann 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; alle suspend-Funktionen müssen in eine Koroutine.

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

  • launch akzeptiert keinen Dispatchers.IO-Parameter. Wenn Sie nicht Dispatcher an launch übergeben, alle Koroutinen, die ab viewModelScope wird im Hauptthread ausgeführt.
  • Das Ergebnis der Netzwerkanfrage wird nun verarbeitet, um den Erfolg Fehler der Benutzeroberfläche.

Die Log-in-Funktion wird jetzt wie folgt ausgeführt:

  • Die App ruft die Funktion login() über die View-Ebene im Hauptthread auf.
  • launch erstellt eine neue Koroutine im Hauptthread und die Koroutine beginnt Ausführung.
  • Innerhalb der Koroutine führt der Aufruf von loginRepository.makeLoginRequest() sperrt jetzt die weitere Ausführung der Koroutine bis withContext Der Block in makeLoginRequest() ist fertig.
  • Sobald der withContext-Block abgeschlossen ist, wird die Koroutine in login() fortgesetzt. Ausführung im Hauptthread mit dem Ergebnis der Netzwerkanfrage

Umgang mit Ausnahmen

Zur Verarbeitung von Ausnahmen, die von der Ebene Repository ausgelöst werden können, verwenden Sie die integrierte Unterstützung für Ausnahmen. 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() wird in der Benutzeroberfläche als Fehler behandelt.

Zusätzliche Ressourcen für Koroutinen

Detailliertere Informationen zu gemeinsamen Abläufen auf Android findest du unter App-Leistung mit Kotlin-Koroutinen verbessern

Weitere Ressourcen zu Koroutinen finden Sie unter den folgenden Links: