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 ViewModel
Arkitekturkomponenten 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 foruddefineretCoroutineScope
, der er inkluderet medViewModel
KTX-udvidelserne. Bemærk, at alle coroutiner skal køre i ascope. EnCoroutineScope
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 fraView
-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 ViewModel
s 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 enDispatchers.IO
parameter. Når du ikke overdrager enDispatcher
tillaunch
, 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 fraView
-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, indtilwithContext
blokken imakeLoginRequest()
er færdig med at køre. - Når
withContext
blokken er færdig, genoptager coroutinen ilogin()
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