Kotlin coroutines på Android
En coroutine är ett designmönster för samtidighet som du kan använda på Android för att förenkla kod som exekveras asynkront.Coroutines lades till i Kotlin i version 1.3 och är baserade på etablerade koncept från andra språk.
På Android hjälper coroutines till att hantera långvariga uppgifter som annars skulle kunna blockera huvudtråden och göra att appen inte reagerar.Över 50 % av professionella utvecklare som använder coroutines har rapporterat om ökad produktivitet.Det här ämnet beskriver hur du kan använda Kotlin coroutines för att lösa dessa problem, så att du kan skriva renare och mer koncis appkod.
Funktioner
Coroutines är vår rekommenderade lösning för asynkron programmering påAndroid. Bland de anmärkningsvärda funktionerna finns följande:
- Lättvikt: Du kan köra många coroutines på en enda tråd tack vare stöd för suspension, vilket inte blockerar tråden där coroutinen körs. Suspendings sparar minne jämfört med blockering och stöder samtidigt många samtidiga operationer.
- Färre minnesläckor:
- Inbyggt stöd för avbokning: Avbokning sprids automatiskt genom den körda coroutin-hierarkin.
- Jetpack-integration: Många Jetpack-bibliotek innehåller tillägg som ger fullt stöd för coroutiner. Vissabibliotek tillhandahåller också ett egetcoroutineområde som du kan använda för strukturerad samtidighet.
Exempelöversikt
Med utgångspunkt i guiden för apparkitektur gör exemplen i det här avsnittet en nätverksförfrågan och returnerar resultatet till huvudtråden, där appen sedan kan visa resultatet för användaren.
Specifikt anropar komponenten ViewModel
Architecture lagret på huvudtråden för att trigga nätverksförfrågan. Den här guiden går igenom olika lösningar som använder coroutines för att hålla huvudtråden oblockad.
ViewModel
innehåller en uppsättning KTX-tillägg som arbetar direkt med coroutines. Dessa tillägg ärlifecycle-viewmodel-ktx
bibliotek och används i den här guiden.
Dependecy info
För att använda coroutines i ditt Android-projekt lägger du till följande beroende till appens build.gradle
fil:
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'}
Exekveras i en bakgrundstråd
Att göra en nätverksförfrågan på huvudtråden gör att den väntar, eller blockerar,tills den får ett svar. Eftersom tråden är blockerad kan operativsystemet inte anropa onDraw()
, vilket gör att programmet fryser och eventuellt leder till en ANR-dialog (Application Not Responding). För en bättre användarupplevelse kan vi köra den här operationen på en bakgrundstråd.
För det första tar vi en titt på vår Repository
-klass och ser hur den hanterar nätverksbegäran:
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
är synkron och blockerar den anropande tråden. För att modellera svaret på nätverksförfrågan har vi vår egen Result
-klass.
ViewModel
utlöser nätverksförfrågan när användaren klickar, till exempel på en knapp:
class LoginViewModel( private val loginRepository: LoginRepository): ViewModel() { fun login(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) }}
Med den tidigare koden blockerar LoginViewModel
gränssnittstråden när den gör nätverksförfrågan. Den enklaste lösningen för att flytta utförandet från huvudtråden är att skapa en ny coroutine och utföra nätverksbegäran 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) } }}
Låt oss dissekera coroutinkoden i login
-funktionen:
-
viewModelScope
är en fördefinieradCoroutineScope
som ingår medViewModel
KTX-tillägget. Observera att alla koroutiner måste köras i ascope. EnCoroutineScope
hanterar en eller flera relaterade coroutiner. -
launch
är en funktion som skapar en coroutin och skickar utförandet av dess funktionskropp till motsvarande dispatcher. -
Dispatchers.IO
indikerar att denna coroutin ska exekveras på en tråd som är reserverad för I/O-operationer.
Funktionen login
exekveras på följande sätt:
- Appen anropar
login
-funktionen frånView
-skiktet på huvudtråden. -
launch
skapar en ny coroutine och nätverksförfrågan utförs oberoende av varandra på en tråd som är reserverad för I/O-operationer. - Medan coroutinen körs fortsätter
login
-funktionen att exekveras och återkommer, eventuellt innan nätverksförfrågan är klar. Observera att nätverkssvaret för enkelhetens skull ignoreras för tillfället.
Då denna koroutin startas med viewModelScope
, utförs den inom ViewModel
s räckvidd. Om ViewModel
förstörs på grund av att användaren navigerar bort från skärmen avbryts viewModelScope
automatiskt och alla pågående coroutiner avbryts också.
Ett problem med föregående exempel är att allt som anropar makeLoginRequest
måste komma ihåg att uttryckligen flytta utförandet från huvudtråden. Låt oss se hur vi kan modifiera Repository
för att lösa detta problem för oss.
Använd coroutiner för huvudsäkerhet
Vi anser att en funktion är huvudsäker när den inte blockerar uppdateringar av användargränssnittet i huvudtråden. Funktionen makeLoginRequest
är inte huvudsäker, eftersom det blockerar användargränssnittet att anropamakeLoginRequest
från huvudtråden. Använd funktionenwithContext()
från biblioteket coroutines för att flytta utförandet av en coroutine till en annan 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)
flyttar utförandet av coroutinen till en I/O-tråd, vilket gör vår anropande funktion huvudsäker och gör det möjligt för användargränssnittet att uppdateras vid behov.
makeLoginRequest
är också markerad med nyckelordet suspend
. Detta nyckelord är Kotlins sätt att tvinga fram att en funktion ska anropas från en coroutine.
I följande exempel skapas coroutinen i LoginViewModel
.Eftersom makeLoginRequest
flyttar utförandet från huvudtråden kan coroutinen i funktionen login
nu exekveras i huvudtrå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 att coroutinen fortfarande behövs här, eftersom makeLoginRequest
är en suspend
-funktion, och alla suspend
-funktioner måste exekveras i en coroutin.
Denna kod skiljer sig från föregående login
exempel på ett par sätt:
-
launch
tar inte enDispatchers.IO
parameter. När du inte lämnar enDispatcher
tilllaunch
körs alla koroutiner som startas frånviewModelScope
i huvudtråden. - Resultatet av nätverksförfrågan hanteras nu så att det visar UI:et för efterföljande fel.
Inloggningsfunktionen exekveras nu på följande sätt:
- Appen anropar
login()
-funktionen frånView
-skiktet på huvudtråden. -
launch
skapar en ny coroutine för att göra nätverksbegäran på huvudtråden, och coroutinen börjar exekveras. - I coroutinen avbryter anropet till
loginRepository.makeLoginRequest()
now ytterligare exekvering av coroutinen tillswithContext
-blocket imakeLoginRequest()
avslutas. - När
withContext
-blocket avslutas återupptar coroutinen ilogin()
exekveringen på huvudtråden med resultatet av nätverksbegäran.
Hantering av undantag
För att hantera undantag som Repository
-skiktet kan kasta, använder du Kotlins inbyggda stöd för undantag.I följande exempel använder vi ett 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 } } }}
I det här exemplet hanteras alla oväntade undantag som kastas av makeLoginRequest()
-anropet som ett fel i UI.
Leave a Reply