Beste fremgangsmåter for Android Kotlin Coroutine

Dette er et kontinuerlig vedlikeholdt sett med beste fremgangsmåter for bruk av Kotlin Coroutines på Android. Kommenter nedenfor hvis du har noen forslag til noe som bør legges til.

  1. Håndtering av Android-livssykler

På en lignende måte som du bruker CompositeDisposables med RxJava, må Kotlin Coroutines avlyses til rett tid med bevissthet om Android Livecycles med aktiviteter og fragmenter.

a) Bruke Android Viewmodels

Dette er den enkleste måten å sette opp coroutines slik at de blir lagt ned til rett tid, men det fungerer bare inne i en Android ViewModel som har en onCleared-funksjon som koroutinejobber kan avbrytes pålitelig fra:

private val viewModelJob = Jobb ()
private val uiScope = CoroutineScope (Dispatchers.Main + viewModelJob)
overstyre moro onCleared () {
 super.onCleared ()
 uiScope.coroutineContext.cancelChildren ()
}

Merk: Fra ViewModels 2.1.0-alpha01 er dette ikke lenger nødvendig. Du trenger ikke lenger å ha visningsmodellen implementert CoroutineScope, onCleared eller legge til en jobb. Bare bruk “viewModelscope.launch {}”. Legg merke til at 2.x betyr at appen din må være på AndroidX fordi jeg ikke er sikker på at de planlegger å tilbakeportere dette til 1.x-versjonen av ViewModels.

b) Bruke livssyklusobservatorer

Denne andre teknikken skaper et omfang du knytter til en aktivitet eller fragment (eller noe annet som implementerer en Android-livssyklus):

/ **
 * Coroutine-kontekst som automatisk avbrytes når brukergrensesnittet blir ødelagt
 * /
klasse UiLifecycleScope: CoroutineScope, LifecycleObserver {

    privat lateinit var jobb: Jobb
    overstyre val coroutineContext: CoroutineContext
        få () = jobb + Dispatchers.Main

    @OnLifecycleEvent (Lifecycle.Event.ON_START)
    moro onCreate () {
        jobb = jobb ()
    }

    @OnLifecycleEvent (Lifecycle.Event.ON_PAUSE)
    moro ødelegge () = jobb.cancel ()
}
... inne i Support Lib-aktivitet eller fragment
private val uiScope = UiLifecycleScope ()
overstyre moro onCreate (savedInstanceState: bundle) {
  super.onCreate (savedInstanceState)
  lifecycle.addObserver (uiScope)
}

c) GlobalScope

Hvis du bruker GlobalScope, er det et omfang som varer appens levetid. Du vil bruke dette til å gjøre bakgrunnssynkronisering, repo-oppdateringer osv. (Ikke knyttet til en aktivitetssyklus).

d) Tjenester

Tjenester kan kansellere jobbene sine i onDestroy:

private val serviceJob = Jobb ()
private val serviceScope = CoroutineScope (Dispatchers.Main + serviceJob)
overstyre moro onCleared () {
 super.onCleared ()
 serviceJob.cancel ()
}

2. Håndtere unntak

a) I async vs. lansering kontra runBlocking

Det er viktig å merke seg at unntak i en lansering {} -blokk krasjer appen uten en unntakshåndterer. Sett alltid opp en standard unntakshåndterer som skal passere som en parameter for å starte.

Et unntak i en runBlocking {} -blokk vil krasje appen med mindre du legger til en prøvefangst. Legg alltid til en prøve / fangst hvis du bruker runBlocking. Ideelt sett er det bare å bruke runBlocking for enhetstester.

Et unntak kastet i en async {} -blokk vil ikke forplante seg eller kjøres før blokken er ventet fordi den virkelig er en Java-utsatt under. Anropsfunksjonen / metoden skal fange unntak.

b) Fangst unntak

Hvis du bruker async for å kjøre kode som kan kaste unntak, må du pakke koden i et coroutineScope for å fange unntak ordentlig (takk til LouisC for eksempel):

prøv {
    coroutineScope {
        val mayFailAsync1 = async {
            mayFail1 ()
        }
        val mayFailAsync2 = async {
            mayFail2 ()
        }
        useResult (mayFailAsync1.await (), mayFailAsync2.await ())
    }
} fangst (e: IOException) {
    // håndtere dette
    kaste MyIoException ("Feil ved å gjøre IO", e)
} fangst (e: AnotherException) {
    // takle dette også
    kaste MyOtherException ("Feil ved å gjøre noe", e)
}

Når du tar unntaket, kan du pakke det inn i et annet unntak (ligner det du gjør for RxJava), slik at du får stacktrace-linjen i din egen kode i stedet for å se en stacktrace med bare coroutine-kode.

c) Unntak for logging

Hvis du bruker GlobalScope.launch eller en skuespiller, må du alltid gi en unntaksbehandler som kan logge unntak. F.eks

val errorHandler = CoroutineExceptionHandler {_, unntak ->
  // logg til Crashlytics, logcat, etc.
}
val job = GlobalScope.launch (errorHandler) {
...
}

Nesten alltid bør du strukturerte omfang på Android, og en behandler skal brukes:

val errorHandler = CoroutineExceptionHandler {_, unntak ->
  // logg til Crashlytics, logcat, etc .; kan injiseres avhengighet
}
val veileder = SupervisorJob () // kansellert m / Activity Lifecycle
med (CoroutineScope (coroutineContext + veileder)) {
  val noe = lansering (errorHandler) {
    ...
  }
}

Og hvis du bruker async og venter, må du alltid pakke inn prøve / fangst som beskrevet ovenfor, men logg inn etter behov.

d) Vurder Resultat / Feil forseglet klasse

Vurder å bruke en forseglet resultatklasse som kan inneholde en feil i stedet for å kaste unntak:

forseglet klasse Resultat  {
  dataklasse Suksess (valdata: T): Resultat ()
  dataklasse Feil (val-feil: E): Resultat ()
}

e) Navn på Coroutine Context

Når du erklærer en asynk lambda, kan du også nevne den slik:

async (CoroutineName ("MyCoroutine")) {}

Hvis du oppretter din egen tråd for å kjøre, kan du også gi den navnet når du oppretter denne trådutføreren:

newSingleThreadContext ( "MyCoroutineThread")

3. Eksekutorpooler og standard størrelse på bassenger

Coroutines er virkelig samarbeidende multitasking (med kompilatorassistanse) på en begrenset trådbassengstørrelse. Det betyr at hvis du gjør noe som blokkerer i koroutinen din (f.eks. Bruker et blokkerings-API), vil du knytte hele tråden til blokkeringsoperasjonen er fullført. Coroutinen vil heller ikke suspendere med mindre du gir utbytte eller forsinkelse, så hvis du har en lang behandlingssløyfe, må du sjekke om koroutinen er kansellert (ring "sikreActive ()" på omfanget) slik at du kan frigjøre tråden; dette ligner på hvordan RxJava fungerer.

Kotlin koroutiner har noen få innebygde sendere (tilsvarer planleggere i RxJava). Hovedkjøreren (hvis du ikke spesifiserer noe å kjøre på) er brukergrensesnittet. Du bør bare endre brukergrensesnittelementer i denne sammenhengen. Det er også en dispatchers.Unconfined som kan hoppe mellom brukergrensesnitt og bakgrunnstråder slik at den ikke er på en enkelt tråd; dette skal vanligvis ikke brukes, bortsett fra i enhetstester. Det er en Dispatchers.IO for IO-håndtering (nettverkssamtaler som avbrytes ofte). Endelig er det en Dispatchers.Default som er den viktigste bakgrunnen tråd pool, men dette er begrenset til antall CPUer.

I praksis bør du bruke et grensesnitt for vanlige sendere som sendes inn via din klassekonstruktør, slik at du kan bytte forskjellige for testing. f.eks .:

grensesnitt CoroutineDispatchers {
  val UI: Dispatcher
  val IO: Dispatcher
  val Computation: Dispatcher
  morsom newThread (val navn: String): Dispatcher
}

4. Unngå datakorrupsjon

Ikke ha suspenderende funksjoner endre data utenfor funksjonen. For eksempel kan dette ha utilsiktet datamodifisering hvis de to metodene kjøres fra forskjellige tråder:

val list = mutableListOf (1, 2)
avbryt moro updateList1 () {
  liste [0] = liste [0] + 1
}
avbryt morsom updateList2 () {
  list.clear ()
}

Du kan unngå denne typen problemer ved å:
- å få koroutinene dine til å returnere et uforanderlig objekt i stedet for å nå ut og endre et
- kjør alle disse koroutiner i en enkelt gjenget kontekst som er opprettet via: newSingleThreadContext (“kontekstnavn”)

5. Gjør Proguard Happy

Disse bør regler må legges til for versjonsbygg av appen din:

-koder på kotlinx.coroutines.internal.MainDispatcherFactory {}
-holder navn på klassen kotlinx.coroutines.CoroutineExceptionHandler {}
-Hold klassene medlemmer kotlinx klasse. ** {flyktige ; }

6. Interop med Java

Hvis du jobber med en gammel app, vil du uten tvil ha en betydelig del av Java-koden. Du kan ringe coroutines fra Java ved å returnere en CompletableFuture (husk å inkludere kotlinx-coroutines-jdk8-artefakt):

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

7. Ettermontering trenger ikke medContext

Hvis du bruker Retrofit coroutines-adapter, får du en utsatt som bruker okhttps async-samtale under panseret. Så du trenger ikke å legge til withContext (Dispatchers.IO) som du vil ha å gjøre med RxJava for å sikre at koden kjører på en IO-tråd; Hvis du ikke bruker Retrofit coroutines adapter og ringer en Retrofit Call direkte, trenger du det medContext.

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

referanser:

  • 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