Kotlin coroutines på Android

En coroutine er et designmønster for samtidighed, som du kan bruge på Android til at forenkle kode, der udføres asynkront.Coroutines blev tilføjet til Kotlin i version 1.3 og er baseret på etablerede koncepter fra andre sprog.

På Android hjælper coroutines med at håndtere langvarige opgaver, der ellers kan blokere hovedtråden og få din app til at blive uopmærksom.Over 50 % af de professionelle udviklere, der bruger coroutines, har rapporteret om øget produktivitet.Dette emne beskriver, hvordan du kan bruge Kotlin-coroutines til at løse disse problemer, så du kan skrive renere og mere kortfattet app-kode.

Funktioner

Coroutines er vores anbefalede løsning til asynkron programmering på Android. Bemærkelsesværdige funktioner omfatter følgende:

  • Letvægt: Du kan køre mange coroutines på en enkelt tråd på grund af understøttelse af suspendering, som ikke blokerer den tråd, hvor coroutinen kører. Suspendering sparer hukommelse i forhold til blokering og understøtter samtidig mange samtidige operationer.
  • Færre hukommelseslækager:
  • Indbygget understøttelse af annullering: Annullering udbredes automatisk gennem det kørende coroutine-hierarki.
  • Jetpack-integration: Mange Jetpack-biblioteker indeholder udvidelser, der giver fuld understøttelse af coroutiner. Noglebiblioteker tilbyder også deres egetcoroutineområde, som du kan bruge til struktureret samtidighed.

Eksempler oversigt

Med udgangspunkt i Vejledning til app-arkitektur foretager eksemplerne i dette emne en netværksanmodning og returnerer resultatet til hovedtråden, hvor appen derefter kan vise resultatet til brugeren.

Specifikt kalder ViewModelArkitekturkomponenten repository-laget på hovedtråden for at udløse netværksanmodningen. Denne vejledning gennemgår forskellige løsninger, der anvender coroutines, så hovedtråden ikke blokeres.

ViewModel indeholder et sæt KTX-udvidelser, der arbejder direkte med coroutines. Disse udvidelser erlifecycle-viewmodel-ktx biblioteket og bruges i denne vejledning.

Info om afhængighed

For at bruge coroutiner i dit Android-projekt skal du tilføje følgende afhængighed til din app’s build.gradle fil:

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

Udføres i en baggrundstråd

Afgivelse af en netværksanmodning på hovedtråden får den til at vente, eller blokere,indtil den modtager et svar. Da tråden er blokeret, skal operativsystemet ikke kalde onDraw(), hvilket får din app til at fryse og potentielt fører til en ANR-dialog (Application Not Responding). For at få en bedre brugeroplevelse kan vi køre denne operation på en baggrundstråd.

Først tager vi et kig på vores Repository-klasse og ser, hvordan den smager netværksanmodningen:

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 er synkron og blokerer den kaldende tråd. For at modellere svaret på netværksanmodningen har vi vores egen Result-klasse.

ViewModel udløser netværksanmodningen, når brugeren klikker, f.eks. på en knap:

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

Med den foregående kode blokerer LoginViewModel UI-tråden, når den foretager netværksanmodningen. Den enkleste løsning til at flytte udførelsen væk fra hovedtråden er at oprette en ny coroutine og udføre netværksanmodningen på en I/O-tråd:

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) } }}

Lad os dissekere coroutinekoden i login-funktionen:

  • viewModelScope er en foruddefineret CoroutineScope, der er inkluderet med ViewModel KTX-udvidelserne. Bemærk, at alle coroutiner skal køre i ascope. En CoroutineScope administrerer en eller flere relaterede coroutiner.
  • launch er en funktion, der opretter en coroutine og sender udførelsen af dens funktionskrop til den tilsvarende dispatcher.
  • Dispatchers.IO angiver, at denne coroutine skal udføres på enthread, der er reserveret til I/O-operationer.

login-funktionen udføres på følgende måde:

  • App’en kalder login-funktionen fra View-laget på hovedtråden.
  • launch opretter en ny coroutine, og netværksanmodningen udføres uafhængigt på en tråd, der er reserveret til I/O-operationer.
  • Mens coroutinen kører, fortsætter login-funktionen udførelsen og vender tilbage, eventuelt før netværksanmodningen er færdig. Bemærk, at netværkssvaret for enkelhedens skyld ignoreres indtil videre.

Da denne coroutine er startet med viewModelScope, udføres den inden for ViewModels rækkevidde. Hvis ViewModel ødelægges, fordi brugeren navigerer væk fra skærmen, annulleres viewModelScope automatisk, og alle kørende coroutiner annulleres også.

Et problem med det foregående eksempel er, at alt, der kalder makeLoginRequest, skal huske at flytte udførelsen eksplicit ud af hovedtråden. Lad os se, hvordan vi kan ændre Repository for at løse dette problem for os.

Brug coroutiner til main-safety

Vi betragter en funktion som main-safe, når den ikke blokerer for opdateringer af brugergrænsefladen påin thread. Funktionen makeLoginRequest er ikke main-safe, da det blokerer brugergrænsefladen at kaldemakeLoginRequest fra hovedtråden. Brug funktionenwithContext() fra coroutines-biblioteket til at flytte udførelsen af en coroutine til en anden tråd:

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) flytter udførelsen af coroutinen til en I/O-tråd, hvilket gør vores kaldende funktion main-safe og muliggør UI-opdatering efter behov.

makeLoginRequest er også markeret med nøgleordet suspend. Dette nøgleord er Kotlins måde at gennemtvinge, at en funktion skal kaldes fra en coroutine.

I det følgende eksempel er coroutinen oprettet i LoginViewModel.Da makeLoginRequest flytter udførelsen væk fra hovedtråden, kan coroutinen i login-funktionen nu udføres i hovedtråden:

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 } } }}

Bemærk, at coroutinen stadig er nødvendig her, da makeLoginRequest er en suspend-funktion, og alle suspend-funktioner skal udføres i en coroutine.

Denne kode adskiller sig fra det foregående login eksempel på et par måder:

  • launch tager ikke en Dispatchers.IO parameter. Når du ikke overdrager en Dispatcher til launch, kører alle coroutiner, der startes fraviewModelScope, i hovedtråden.
  • Resultatet af netværksanmodningen håndteres nu til visning af den fejlbehæftede brugergrænseflade for efterfølgeren.

Loginfunktionen udføres nu på følgende måde:

  • Appen kalder login()-funktionen fra View-laget på hovedtråden.
  • launch opretter en ny coroutine til at foretage netværksanmodningen på hovedtråden, og coroutinen begynder udførelsen.
  • I coroutinen suspenderer kaldet til loginRepository.makeLoginRequest()now den videre udførelse af coroutinen, indtil withContextblokken i makeLoginRequest() er færdig med at køre.
  • Når withContextblokken er færdig, genoptager coroutinen i login() udførelsen på hovedtråden med resultatet af netværksanmodningen.

Håndtering af undtagelser

For at håndtere undtagelser, som Repository-laget kan kaste, skal du bruge Kotlins indbyggede understøttelse af undtagelser.I det følgende eksempel bruger vi en 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 } } }}

I dette eksempel håndteres enhver uventet undtagelse, der kastes af makeLoginRequest()kaldet, som en fejl i brugergrænsefladen.

Leave a Reply