Android Kotlin Coroutine bedste praksis

Det er et kontinuerligt vedligeholdt sæt af bedste fremgangsmåder til brug af Kotlin Coroutines på Android. Kommenter venligst nedenfor, hvis du har forslag til noget, der skal tilføjes.

  1. Håndtering af Android-livscykler

På en lignende måde som du bruger CompositeDisposables med RxJava, skal Kotlin Coroutines annulleres på det rigtige tidspunkt med bevidsthed om Android Livecycles med aktiviteter og fragmenter.

a) Brug af Android Viewmodels

Dette er den nemmeste måde at indstille koroutiner, så de lukker ned på det rigtige tidspunkt, men det fungerer kun i en Android ViewModel, der har en onCleared-funktion, som koroutinejob kan pålideligt fra:

privat val viewModelJob = Job ()
private val uiScope = CoroutineScope (Dispatchers.Main + viewModelJob)
tilsidesætte sjov onCleared () {
 super.onCleared ()
 uiScope.coroutineContext.cancelChildren ()
}

Bemærk: Fra ViewModels 2.1.0-alpha01 er dette ikke længere nødvendigt. Du behøver ikke længere have din viewmodel til at implementere CoroutineScope, onCleared eller tilføje et job. Brug bare “viewModelscope.launch {}”. Bemærk, at 2.x betyder, at din app skal være på AndroidX, fordi jeg ikke er sikker på, at de planlægger at tilbageportere dette til 1.x-versionen af ​​ViewModels.

b) Brug af livscykelobservatører

Denne anden teknik skaber et omfang, som du knytter til en aktivitet eller fragment (eller noget andet, der implementerer en Android-livscyklus):

/ **
 * Coroutine-kontekst, der automatisk annulleres, når brugergrænsefladen ødelægges
 * /
klasse UiLifecycleScope: CoroutineScope, LifecycleObserver {

    privat lateinit var job: Job
    tilsidesætte val coroutineContext: CoroutineContext
        get () = job + Dispatchers.Main

    @OnLifecycleEvent (Lifecycle.Event.ON_START)
    sjov onCreate () {
        job = job ()
    }

    @OnLifecycleEvent (Lifecycle.Event.ON_PAUSE)
    sjov ødelægge () = job.cancel ()
}
... inden i Support Lib-aktivitet eller fragment
privat val uiScope = UiLifecycleScope ()
tilsidesætte sjov onCreate (gemtInstanceState: bundle) {
  super.onCreate (savedInstanceState)
  lifecycle.addObserver (uiScope)
}

c) GlobalScope

Hvis du bruger GlobalScope, er det et omfang, der varer appens levetid. Du vil bruge dette til at udføre baggrundssynkronisering, gentage opdateringer osv. (Ikke bundet til en aktivitetscyklus).

d) Tjenester

Tjenester kan annullere deres job i onDestroy:

privat val serviceJob = Job ()
private val serviceScope = CoroutineScope (Dispatchers.Main + serviceJob)
tilsidesætte sjov onCleared () {
 super.onCleared ()
 serviceJob.cancel ()
}

2. Håndtering af undtagelser

a) I async vs. lancering vs. runBlocking

Det er vigtigt at bemærke, at undtagelser i en lancering {} -blok vil ødelægge appen uden en undtagelseshåndterer. Sæt altid en standard undtagelseshåndterer til at videregive som en parameter, der skal startes.

En undtagelse inden for en runBlocking {} -blok ødelægger appen, medmindre du tilføjer en trykfangst. Tilføj altid en prøve / fangst, hvis du bruger runBlocking. Brug ideelt set runBlocking til enhedstest.

En undtagelse, der kastes inden for en async {} -blok, vil ikke udbrede sig eller køre, før blokken afventes, fordi den virkelig er en Java-udskudt nedenunder. Opkaldsfunktionen / metoden skal fange undtagelser.

b) Fangende undtagelser

Hvis du bruger async til at køre kode, der kan kaste undtagelser, er du nødt til at indpakke koden i et coroutineScope for at fange undtagelser korrekt (tak til LouisC for eksemplet):

prøve {
    coroutineScope {
        val mayFailAsync1 = async {
            mayFail1 ()
        }
        val mayFailAsync2 = async {
            mayFail2 ()
        }
        useResult (mayFailAsync1.await (), mayFailAsync2.await ())
    }
} fangst (e: IOException) {
    // håndtere dette
    smid MyIoException ("Fejl ved udførelse af IO", e)
} fangst (e: AnotherException) {
    // også håndtere dette
    smid MyOtherException ("Fejl ved at gøre noget", e)
}

Når du finder undtagelsen, skal du pakke den ind i en anden undtagelse (svarende til hvad du gør for RxJava), så du får stacktrace-linjen i din egen kode i stedet for at se en stacktrace med kun coroutine-kode.

c) Undtagelser til logning

Hvis du bruger GlobalScope.launch eller en skuespiller, skal du altid indtaste en undtagelseshandler, der kan logge undtagelser. For eksempel.

val errorHandler = CoroutineExceptionHandler {_, undtagelse ->
  // log til Crashlytics, logcat osv.
}
val job = GlobalScope.launch (errorHandler) {
...
}

Næsten altid skal du strukturere scopes på Android, og en håndterer skal bruges:

val errorHandler = CoroutineExceptionHandler {_, undtagelse ->
  // logge på Crashlytics, logcat osv .; kan injiceres afhængighed
}
val supervisor = SupervisorJob () // annulleret w / Activity Lifecycle
med (CoroutineScope (coroutineContext + vejleder)) {
  val noget = lancering (errorHandler) {
    ...
  }
}

Og hvis du bruger async og afventer, skal du altid indpakke prøve / fangst som beskrevet ovenfor, men logge efter behov.

d) Overvej resultat / fejl forseglet klasse

Overvej at bruge en forseglet klasse, der kan indeholde en fejl i stedet for at kaste undtagelser:

forseglet klasse Resultat  {
  dataklasse Succes (valdata: T): Resultat ()
  dataklasse Fejl (val-fejl: E): Resultat ()
}

e) Navn på Coroutine-kontekst

Når du erklærer en async lambda, kan du også navngive den sådan:

async (CoroutineName ("MyCoroutine")) {}

Hvis du opretter din egen tråd til at køre i, kan du også navngive den, når du opretter denne tråd eksekvering:

newSingleThreadContext ( "MyCoroutineThread")

3. Eksekutorpuljer og standardpulstørrelser

Coroutines er virkelig kooperativ multitasking (med kompilatorhjælp) på en begrænset gevindpulstørrelse. Det betyder, at hvis du gør noget, der blokerer i din coroutine (f.eks. Bruger et blokerende API), vil du binde hele tråden op, indtil blokeringsoperationen er udført. Coroutinen suspenderer heller ikke, medmindre du foretager et udbytte eller forsinkelse, så hvis du har en lang forarbejdningssløjfe, skal du sørge for at kontrollere, om koroutinen er blevet annulleret (kald "sikreActive ()" på omfanget), så du kan frigøre tråden; dette ligner, hvordan RxJava fungerer.

Kotlin coroutines har et par indbyggede afsendere (svarende til planlægere i RxJava). Den vigtigste afsender (hvis du ikke specificerer noget at køre på) er UI-en; du skal kun ændre UI-elementer i denne sammenhæng. Der er også en Dispatchers.Unconfined, der kan hoppe mellem UI og baggrundstråde, så det ikke er på en enkelt tråd; dette bør generelt ikke bruges undtagen i enhedstest. Der er en Dispatchers.IO til IO-håndtering (netværksopkald, der ofte suspenderes). Endelig er der en Dispatchers.Default, som er den vigtigste baggrundstrådpulje, men dette er begrænset til antallet af CPU'er.

I praksis skal du bruge en grænseflade til almindelige sendere, der sendes ind via din klasses konstruktør, så du kan bytte forskellige til test. For eksempel.:

interface CoroutineDispatchers {
  val UI: Dispatcher
  val IO: Dispatcher
  val Computation: Dispatcher
  sjove newThread (val navn: String): Dispatcher
}

4. Undgå datakorruption

Undlad at suspendere funktioner ændrer data uden for funktionen. For eksempel kan dette have utilsigtet datamodifikation, hvis de to metoder køres fra forskellige tråde:

val list = mutableListOf (1, 2)
suspendere sjov opdateringListe1 () {
  liste [0] = liste [0] + 1
}
afbryd sjov updateList2 () {
  list.clear ()
}

Du kan undgå denne type problemer ved at:
- Når dine koroutiner returnerer et uforanderligt objekt i stedet for at nå ud og ændre et
- kør alle disse koroutiner i en enkelt trådet kontekst, der er oprettet via: newSingleThreadContext (“kontekstnavn”)

5. Gør Proguard glad

Disse skal regler tilføjes for frigivelsesopbygning af din app:

-opkaldsnavn kotlinx.coroutines.internal.MainDispatcherFactory {}
-opbevar klasser kotlinx.coroutines.CoroutineExceptionHandler {}
- holdeklassemedlemmernavner klasse kotlinx. ** {flygtige ; }

6. Interop med Java

Hvis du arbejder på en ældre app, har du uden tvivl en betydelig del af Java-kode. Du kan kalde coroutines fra Java ved at returnere en CompletableFuture (husk at inkludere kotlinx-coroutines-jdk8-artefakten):

doSomethingAsync (): CompletableFuture > =
   GlobalScope.future {doSomething ()}

7. Eftermontering behøver ikke medContext

Hvis du bruger Retrofit coroutines-adapteren, får du en udsat, der bruger okhttps async-opkald under hætten. Så du behøver ikke at tilføje medContext (Dispatchers.IO), som du har at gøre med RxJava for at sikre dig, at koden kører på en IO-tråd; Hvis du ikke bruger Retrofit coroutines-adapteren og ringer til et Retrofit-opkald direkte, har du brug for medContext.

Android Arch Components Room DB fungerer også automatisk i en ikke-UI-kontekst, så du behøver ikke withContext.

Referencer:

  • https://medium.com/capital-one-tech/kotlin-coroutines-on-android-things-i-wish-i-knew-at-the-beginning-c2f0b1f16cff
  • https://speakerdeck.com/elizarov/fresh-async-with-kotlin
  • https://medium.com/@michaelbukachi/coroutines-and-idling-resources-c1866bfa5b5d
  • https://blog.kotlin-academy.com/kotlin-coroutines-cheat-sheet-8cf1e284dc35
  • https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5?linkId=63267803
  • https://proandroiddev.com/managing-exceptions-in-nested-coroutine-scopes-9f23fd85e61