Kotlin coroutines op Android
Een coroutine is een concurrency ontwerppatroon dat u kunt gebruiken op Android om code die asynchroon wordt uitgevoerd te vereenvoudigen.Coroutines werden toegevoegd aan Kotlin in versie 1.3 en zijn gebaseerd op gevestigde concepten uit andere talen.
Op Android helpen coroutines bij het beheren van langlopende taken die anders de hoofddraad zouden kunnen blokkeren en ervoor zorgen dat uw app niet meer reageert.Meer dan 50% van de professionele ontwikkelaars die coroutines gebruiken, hebben gemeld dat hun productiviteit is toegenomen.Dit onderwerp beschrijft hoe u Kotlin coroutines kunt gebruiken om deze problemen aan te pakken, zodat u schonere en beknoptere app-code kunt schrijven.
Functies
Coroutines is onze aanbevolen oplossing voor asynchroon programmeren op Android. Opmerkelijke functies zijn onder andere de volgende:
- Lichtgewicht: U kunt veel coroutines draaien op een enkele thread als gevolg van de ondersteuning voor opschorten, die niet blokkeert de thread waar de coroutine wordt uitgevoerd. Suspendings bespaart geheugen ten opzichte van blocking en ondersteunt veel gelijktijdige operaties.
- Minder geheugenlekken: Gebruik gestructureerde concurrency om bewerkingen binnen een scope uit te voeren.
- Ingebouwde annuleringsondersteuning:Annulering wordt automatisch gepropageerd door de lopende coroutinehiërarchie.
- Jetpack-integratie: Veel Jetpack-bibliotheken bevatten extensies die volledige ondersteuning bieden voor coroutines. Sommige bibliotheken bieden ook hun eigen coroutinescope die u kunt gebruiken voor gestructureerde concurrency.
Exemplarenoverzicht
Gebaseerd op de Gids voor app-architectuur, doen de voorbeelden in dit onderwerp een netwerkverzoek en retourneren het resultaat naar de mainthread, waar de app vervolgens het resultaat aan de gebruiker kan tonen.
Specifiek roept het ViewModel
Architecture-component de repository-laag op de mainthread aan om het netwerkverzoek te triggeren. In deze handleiding worden verschillende oplossingen besproken die gebruikmaken van coroutines om de hoofddraad te deblokkeren.
ViewModel
bevat een set KTX-extensies die rechtstreeks met coroutines werken. Deze extensies zijnlifecycle-viewmodel-ktx
bibliotheek en worden in deze gids gebruikt.
Afhankelijkheidsinformatie
Om coroutines in uw Android-project te gebruiken, voegt u de volgende afhankelijkheid toe aan het build.gradle
bestand van uw app:
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'}
Uitvoeren in een achtergronddraad
Het doen van een netwerkverzoek op de hoofddraad zorgt ervoor dat deze wacht, of blokkeert, totdat deze een antwoord ontvangt. Omdat de thread geblokkeerd is, is het OS niet in staat om onDraw()
op te roepen, wat ervoor zorgt dat uw app bevriest en mogelijk leidt tot een Application Not Responding (ANR) dialoog. Voor een betere gebruikservaring, laten we deze bewerking op een achtergronddraad uitvoeren.
Laten we eerst eens kijken naar onze Repository
klasse en zien hoe deze het netwerkverzoek smakt:
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
is synchroon en blokkeert de aanroepende thread. Om de reactie van het netwerk verzoek te modelleren, hebben we onze eigen Result
klasse.
De ViewModel
triggert het netwerk verzoek wanneer de gebruiker klikt, bijvoorbeeld, op een knop:
class LoginViewModel( private val loginRepository: LoginRepository): ViewModel() { fun login(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) }}
Met de vorige code, LoginViewModel
blokkeert de UI thread bij het maken van het netwerk verzoek. De eenvoudigste oplossing om de uitvoering van de hoofddraad af te halen is om een nieuwe coroutine te maken en het netwerkverzoek op een I/O thread uit te voeren:
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) } }}
Laten we de coroutine code in de login
functie ontleden:
-
viewModelScope
is een voorgedefinieerdeCoroutineScope
die is opgenomen met deViewModel
KTX uitbreidingen. Merk op dat alle coroutines in ascope moeten worden uitgevoerd. EenCoroutineScope
beheert een of meer gerelateerde coroutines. -
launch
is een functie die een coroutine creëert en de uitvoering van de body van de functie naar de corresponderende dispatcher stuurt. -
Dispatchers.IO
geeft aan dat deze coroutine moet worden uitgevoerd op een athread die is gereserveerd voor I/O-bewerkingen.
De login
-functie wordt als volgt uitgevoerd:
- De app roept de
login
-functie aan vanuit deView
-laag op de hoofddraad. -
launch
creëert een nieuwe coroutine, en het netwerkverzoek wordt onafhankelijk gedaan op een thread die is gereserveerd voor I/O-bewerkingen. - Terwijl de coroutine draait, gaat de
login
-functie door met de uitvoering en keert terug, mogelijk voordat het netwerkverzoek is voltooid. Merk op dat voor de eenvoud, het netwerk antwoord voor nu wordt genegeerd.
Omdat deze coroutine is gestart met viewModelScope
, wordt deze uitgevoerd binnen het bereik van de ViewModel
. Als de ViewModel
wordt vernietigd omdat de gebruiker weg van het scherm navigeert, wordt viewModelScope
automatisch geannuleerd, en alle lopende coroutines worden ook geannuleerd.
Eén probleem met het vorige voorbeeld is dat alles watmakeLoginRequest
aanroept, moet onthouden om de uitvoering expliciet van de hoofddraad af te halen. Laten we eens kijken hoe we Repository
kunnen aanpassen om dit probleem voor ons op te lossen.
Gebruik coroutines voor main-safety
We beschouwen een functie als main-safe wanneer het geen UI updates blokkeert op de main thread. De functie makeLoginRequest
is niet main-safe, omdat het aanroepen vanmakeLoginRequest
vanuit de main thread de UI wel blokkeert. Gebruik dewithContext()
functie uit de coroutines bibliotheek om de uitvoering van een coroutine naar een andere thread te verplaatsen:
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)
verplaatst de uitvoering van de coroutine naar een I/O thread, waardoor onze aanroepende functie main-safe wordt en de UI kan worden bijgewerkt als dat nodig is.
makeLoginRequest
is ook gemarkeerd met het suspend
sleutelwoord. Dit sleutelwoord is Kotlin’s manier om een functie af te dwingen om te worden aangeroepen vanuit een coroutine.
In het volgende voorbeeld, is de coroutine gemaakt in de LoginViewModel
.Aangezien makeLoginRequest
de uitvoering verplaatst van de hoofddraad, kan de coroutine in de login
functie nu worden uitgevoerd in de hoofddraad:
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 } } }}
Merk op dat de coroutine hier nog steeds nodig is, omdat makeLoginRequest
een suspend
functie is, en alle suspend
functies moeten worden uitgevoerd in een coroutine.
Deze code verschilt van het vorige login
voorbeeld op een paar manieren:
-
launch
neemt geenDispatchers.IO
parameter. Wanneer u geenDispatcher
aanlaunch
doorgeeft, worden alle coroutines die vanuitviewModelScope
worden gestart in de hoofddraad uitgevoerd. - Het resultaat van het netwerkverzoek wordt nu verwerkt om de opvolger failure UI weer te geven.
De login-functie wordt nu als volgt uitgevoerd:
- De app roept de
login()
-functie van deView
-laag op de hoofddraad aan. -
launch
maakt een nieuwe coroutine om het netwerkverzoek op de hoofdthread te doen, en de coroutine begint met de uitvoering. - In de coroutine, de oproep aan
loginRepository.makeLoginRequest()
nu onderbreekt verdere uitvoering van de coroutine totdat hetwithContext
-blok inmakeLoginRequest()
klaar is met draaien. - Zodra het
withContext
-blok klaar is, hervat de coroutine inlogin()
de uitvoering op de hoofddraad met het resultaat van het netwerkverzoek.
Handelen van uitzonderingen
Om uitzonderingen af te handelen die de Repository
laag kan gooien, gebruikt u Kotlin’s ingebouwde ondersteuning voor uitzonderingen.In het volgende voorbeeld gebruiken we een try-catch
blok:
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 dit voorbeeld wordt elke onverwachte uitzondering die door de makeLoginRequest()
aanroep wordt gegooid, afgehandeld als een fout in de UI.
Leave a Reply