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 ViewModel
Architekturkomponente 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.
ViewModel
umfasst eine Reihe von KTX-Erweiterungen, die direkt mit Coroutines arbeiten. Diese Erweiterungen sindlifecycle-viewmodel-ktx
Bibliotheken 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.gradle
Datei 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 vordefinierteCoroutineScope
, die in denViewModel
KTX-Erweiterungen enthalten ist. Beachten Sie, dass alle Koroutinen in einem Bereich ablaufen müssen. EineCoroutineScope
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 derView
-Schicht auf dem Haupt-Thread auf. -
launch
erzeugt 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 login
Beispiel in einigen Punkten:
-
launch
nimmt keinenDispatchers.IO
Parameter entgegen. Wenn SieDispatcher
nicht anlaunch
übergeben, werden alle vonviewModelScope
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 SchichtView
im Hauptthread auf. -
launch
Eine 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 derwithContext
Block inmakeLoginRequest()
beendet ist. - Nach Beendigung des
withContext
Blocks nimmt die Coroutine inlogin()
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