Kotlin coroutines on Android

Eine Coroutine ist ein Gleichzeitigkeitsdesignmuster, das Sie aufAndroid verwenden können, um Code zu vereinfachen, der asynchron ausgeführt wird.Coroutines wurden in Version 1.3 zu Kotlin hinzugefügt und basieren auf etablierten Konzepten aus anderen Sprachen.

Auf Android helfen Coroutines dabei, lang laufende Aufgaben zu verwalten, die andernfalls den Hauptthread blockieren und dazu führen könnten, dass Ihre App nicht mehr reagiert.Mehr als 50% der professionellen Entwickler, die Coroutines verwenden, berichten von einer erhöhten Produktivität.

Features

Coroutines ist unsere empfohlene Lösung für asynchrone Programmierung auf Android. Zu den bemerkenswerten Eigenschaften gehören die folgenden:

  • Leichtgewichtig: Sie können viele Coroutines auf einem einzigen Thread ausführen, da der Thread, auf dem die Coroutine läuft, nicht blockiert wird. Suspending spart Speicher gegenüber Blocking und unterstützt viele gleichzeitige Operationen.
  • Weniger Speicherlecks: Verwenden Sie strukturierte Gleichzeitigkeit, um Operationen innerhalb eines Bereichs auszuführen.
  • Integrierte Unterstützung für Abbrüche: Abbrüche werden automatisch durch die laufende Coroutine-Hierarchie propagiert.
  • Jetpack-Integration: Viele Jetpack-Bibliotheken enthalten Erweiterungen, die vollständige Coroutine-Unterstützung bieten. Einige Bibliotheken bieten auch ihren eigenen Coroutine-Bereich, den Sie für strukturierte Parallelität verwenden können.

Beispiele im Überblick

Basierend auf dem Leitfaden zur App-Architektur stellen die Beispiele in diesem Thema eine Netzwerkanforderung und geben das Ergebnis an den Hauptthread zurück, wo die App dann das Ergebnis dem Benutzer anzeigen kann.

Speziell ruft die ViewModelArchitekturkomponente die Repository-Schicht im Hauptthread auf, um die Netzwerkanforderung auszulösen. Dieser Leitfaden führt durch verschiedene Lösungen, die Coroutines verwenden, um den Hauptthread nicht zu blockieren.

ViewModelumfasst eine Reihe von KTX-Erweiterungen, die direkt mit Coroutines arbeiten. Diese Erweiterungen sindlifecycle-viewmodel-ktxBibliotheken und werden in diesem Handbuch verwendet.

Abhängigkeitsinformationen

Um Coroutines in Ihrem Android-Projekt zu verwenden, fügen Sie die folgende Abhängigkeit zur build.gradleDatei Ihrer App hinzu:

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

Ausführung in einem Hintergrund-Thread

Wenn der Haupt-Thread eine Netzwerkanforderung stellt, muss er warten oder blockieren, bis er eine Antwort erhält. Da der Thread blockiert ist, muss das Betriebssystem onDraw() aufrufen, was dazu führt, dass Ihre Anwendung einfriert und möglicherweise einen ANR-Dialog (Application Not Responding) auslöst. Für eine bessere Benutzererfahrung führen wir diesen Vorgang in einem Hintergrund-Thread aus.

Zunächst werfen wir einen Blick auf unsere Klasse Repository und sehen, wie sie die Netzwerkanforderung ausführt:

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 Netzwerkanforderung zu modellieren, haben wir unsere eigene Klasse Result.

Die Klasse ViewModel löst die Netzwerkanforderung aus, wenn der Benutzer z. B. 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) }}

Mit dem vorherigen Code blockiert LoginViewModel den UI-Thread, wenn die Netzwerkanforderung erfolgt. Die einfachste Lösung, um die Ausführung aus dem Hauptthread zu verlagern, besteht darin, eine neue Coroutine zu erstellen und die Netzwerkanforderung 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) } }}

Zerlegen wir den Code der Coroutine in der Funktion login:

  • viewModelScope ist eine vordefinierte CoroutineScope, die in den ViewModel KTX-Erweiterungen enthalten ist. Beachten Sie, dass alle Koroutinen in einem Bereich ablaufen müssen. Eine CoroutineScope verwaltet eine oder mehrere verwandte Coroutines.
  • launch ist eine Funktion, die eine Coroutine erstellt und die Ausführung ihres Funktionskörpers an den entsprechenden Dispatcher weiterleitet.
  • Dispatchers.IO gibt an, dass diese Coroutine in einem für E/A-Operationen reservierten Thread ausgeführt werden soll.

Die login-Funktion wird wie folgt ausgeführt:

  • Die App ruft die login-Funktion aus der View-Schicht auf dem Haupt-Thread auf.
  • launcherzeugt eine neue Coroutine, und die Netzwerkanforderung wird unabhängig auf einem für E/A-Operationen reservierten Thread ausgeführt.
  • Während die Coroutine läuft, setzt die login-Funktion die Ausführung fort und kehrt zurück, möglicherweise bevor die Netzwerkanforderung beendet ist. Der Einfachheit halber wird die Netzwerkantwort vorerst ignoriert.

Da diese Coroutine mit viewModelScope gestartet wird, wird sie innerhalb des Bereichs der ViewModel ausgeführt. Wenn ViewModel zerstört wird, weil der Benutzer vom Bildschirm weg navigiert, wird viewModelScope automatisch abgebrochen, und alle laufenden Coroutines werden ebenfalls abgebrochen.

Ein Problem mit dem vorherigen Beispiel ist, dass alles, was makeLoginRequest aufruft, daran denken muss, die Ausführung explizit aus dem Hauptthread zu verschieben. Schauen wir uns an, wie wir Repository modifizieren können, um dieses Problem für uns zu lösen.

Verwenden Sie Coroutines für die Hauptsicherheit

Wir betrachten eine Funktion als hauptsicher, wenn sie UI-Aktualisierungen auf dem Hauptthread nicht blockiert. Die Funktion makeLoginRequest ist nicht „main-safe“, da der Aufruf von makeLoginRequest aus dem Haupt-Thread die Benutzeroberfläche blockiert. Verwenden Sie die FunktionwithContext() aus der Coroutines-Bibliothek, um die Ausführung einer Coroutine 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 Coroutine in einenI/O-Thread, wodurch unsere aufrufende Funktion main-safe wird und die UI-Aktualisierung nach Bedarf ermöglicht wird.

makeLoginRequest ist außerdem mit dem Schlüsselwort suspend gekennzeichnet. Mit diesem Schlüsselwort erzwingt Kotlin, dass eine Funktion innerhalb einer Coroutine aufgerufen wird.

Im folgenden Beispiel wird die Coroutine in LoginViewModel erstellt.Da makeLoginRequest die Ausführung aus dem Hauptthread verlagert, kann die Coroutine in der Funktion login nun 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 } } }}

Beachten Sie, dass die Coroutine hier immer noch benötigt wird, da makeLoginRequest eine suspend-Funktion ist und alle suspend-Funktionen in einer Coroutine ausgeführt werden müssen.

Dieser Code unterscheidet sich vom vorherigen loginBeispiel in einigen Punkten:

  • launch nimmt keinen Dispatchers.IOParameter entgegen. Wenn Sie Dispatcher nicht an launch übergeben, werden alle von viewModelScope gestarteten Coroutines im Hauptthread ausgeführt.
  • Das Ergebnis der Netzwerkanforderung wird jetzt so behandelt, dass die Benutzeroberfläche für den Nachfolger angezeigt wird.

Die Anmeldefunktion wird nun wie folgt ausgeführt:

  • Die App ruft die Funktion login() aus der Schicht View im Hauptthread auf.
  • launchEine neue Coroutine wird erstellt, um die Netzwerkanforderung im Hauptthread auszuführen, und die Coroutine beginnt mit der Ausführung.
  • Innerhalb der Coroutine unterbricht der Aufruf von loginRepository.makeLoginRequest()jetzt die weitere Ausführung der Coroutine, bis der withContextBlock in makeLoginRequest() beendet ist.
  • Nach Beendigung des withContextBlocks nimmt die Coroutine in login()die Ausführung im Hauptthread mit dem Ergebnis der Netzwerkanfrage wieder auf.

Handhabung von Ausnahmen

Um Ausnahmen zu behandeln, die die Repository-Schicht auslösen kann, verwenden Sie die in Kotlin eingebaute Unterstützung für Ausnahmen.

class LoginViewModel( private val loginRepository: LoginRepository): ViewModel() { fun makeLoginRequest(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 } } }}

Im folgenden Beispiel verwenden wir einen try-catch-Block:

class LoginViewModel( private val loginRepository: LoginRepository): ViewModel() { fun makeLoginRequest(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 durch den makeLoginRequest()Aufruf ausgelöst wird, als Fehler in der Benutzeroberfläche behandelt.

Leave a Reply