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 ViewModelArchitecture 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ördefinierad CoroutineScope som ingår med ViewModel KTX-tillägget. Observera att alla koroutiner måste köras i ascope. En CoroutineScope 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ån View-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 ViewModels 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 en Dispatchers.IO parameter. När du inte lämnar en Dispatcher till launch 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ån View-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 tills withContext-blocket i makeLoginRequest() avslutas.
  • När withContext-blocket avslutas återupptar coroutinen i login() 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