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 ViewModelArchitecture-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 voorgedefinieerde CoroutineScope die is opgenomen met de ViewModel KTX uitbreidingen. Merk op dat alle coroutines in ascope moeten worden uitgevoerd. Een CoroutineScope 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 de View-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 geen Dispatchers.IO parameter. Wanneer u geen Dispatcher aan launch 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 de View-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 het withContext-blok in makeLoginRequest() klaar is met draaien.
  • Zodra het withContext-blok klaar is, hervat de coroutine in login() 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