Praktische tips voor Android Kotlin Coroutine

Het is een continu onderhouden reeks best practices voor het gebruik van Kotlin Coroutines op Android. Geef hieronder een reactie als u suggesties heeft voor iets dat moet worden toegevoegd.

  1. Omgaan met Android Lifecycles

Op dezelfde manier dat u CompositeDisposables met RxJava gebruikt, moeten Kotlin Coroutines op het juiste moment worden geannuleerd met kennis van Android Livecycles met activiteiten en fragmenten.

a) Android Viewmodels gebruiken

Dit is de eenvoudigste manier om coroutines in te stellen zodat ze op het juiste moment worden afgesloten, maar het werkt alleen in een Android ViewModel met een onCleared-functie waarmee coroutine-taken betrouwbaar kunnen worden geannuleerd:

private val viewModelJob = Job ()
private val uiScope = CoroutineScope (Dispatchers.Main + viewModelJob)
plezier opheffen onCleared () {
 super.onCleared ()
 uiScope.coroutineContext.cancelChildren ()
}

Opmerking: vanaf ViewModels 2.1.0-alpha01 is dit niet langer nodig. U hoeft uw viewmodel niet langer CoroutineScope te laten implementeren, onCleared of een taak toe te voegen. Gebruik gewoon "viewModelscope.launch {}". Merk op dat 2.x betekent dat uw app op AndroidX moet staan, omdat ik niet zeker weet of ze dit willen backporteren naar de 1.x-versie van ViewModels.

b) Lifecycle Observers gebruiken

Deze andere techniek maakt een scope die u koppelt aan een activiteit of fragment (of iets anders dat een Android Lifecycle implementeert):

/ **
 * Coroutine context die automatisch wordt geannuleerd wanneer UI wordt vernietigd
 * /
class UiLifecycleScope: CoroutineScope, LifecycleObserver {

    private lateinit var baan: Job
    val coroutineContext overschrijven: CoroutineContext
        get () = job + Dispatchers.Main

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

    @OnLifecycleEvent (Lifecycle.Event.ON_PAUSE)
    plezier destroy () = job.cancel ()
}
... in Support Lib Activity of Fragment
private val uiScope = UiLifecycleScope ()
plezier overschrijven onCreate (opgeslagenInstanceState: bundle) {
  super.onCreate (savedInstanceState)
  lifecycle.addObserver (uiScope)
}

c) GlobalScope

Als u GlobalScope gebruikt, is dit een scope die de levensduur van de app meegaat. U zou dit gebruiken voor achtergrondsynchronisatie, repo-vernieuwingen, enz. (Niet gekoppeld aan een activiteitenlevenscyclus).

d) Diensten

Services kunnen hun taken in de onDestroy annuleren:

private val serviceJob = Job ()
private val serviceScope = CoroutineScope (Dispatchers.Main + serviceJob)
plezier opheffen onCleared () {
 super.onCleared ()
 serviceJob.cancel ()
}

2. Behandeling van uitzonderingen

a) In async versus lancering versus runBlocking

Het is belangrijk op te merken dat uitzonderingen in een startblok {} de app laten crashen zonder uitzonderingshandler. Stel altijd een standaarduitzonderingshandler in om door te geven als een parameter om te starten.

Een uitzondering binnen een runBlocking {} -blok crasht de app tenzij u een try-catch toevoegt. Voeg altijd een try / catch toe als u runBlocking gebruikt. Gebruik idealiter alleen runBlocking voor unit-tests.

Een uitzondering die in een async {} -blok wordt gegooid, wordt niet doorgevoerd of uitgevoerd totdat het blok wordt afgewacht omdat het hier echt een Java Deferred is. De aanroepende functie / methode moet uitzonderingen bevatten.

b) Vangen van uitzonderingen

Als u async gebruikt om code uit te voeren die uitzonderingen kan veroorzaken, moet u de code in een coroutineScope wikkelen om uitzonderingen op te vangen (met dank aan LouisC voor het voorbeeld):

proberen {
    coroutineScope {
        val mayFailAsync1 = async {
            mayFail1 ()
        }
        val mayFailAsync2 = async {
            mayFail2 ()
        }
        useResult (mayFailAsync1.await (), mayFailAsync2.await ())
    }
} catch (e: IOException) {
    // ga hier mee om
    throw MyIoException ("Error doing IO", e)
} catch (e: AnotherException) {
    // behandel dit ook
    gooi MyOtherException ("Fout bij het doen van iets", e)
}

Wanneer u de uitzondering opvangt, verpakt u deze in een andere uitzondering (vergelijkbaar met wat u doet voor RxJava) zodat u de stacktrace-regel in uw eigen code krijgt in plaats van een stacktrace met alleen coroutinecode.

c) Uitzonderingen voor logboekregistratie

Als u GlobalScope.launch of een acteur gebruikt, geef dan altijd een uitzonderingshandler door die uitzonderingen kan vastleggen. Bijv.

val errorHandler = CoroutineExceptionHandler {_, uitzondering ->
  // log in op Crashlytics, logcat, etc.
}
val job = GlobalScope.launch (errorHandler) {
...
}

Bijna altijd moet u gestructureerde scopes op Android gebruiken en moet een handler worden gebruikt:

val errorHandler = CoroutineExceptionHandler {_, uitzondering ->
  // log in op Crashlytics, logcat, enz .; kan afhankelijkheid worden geïnjecteerd
}
val supervisor = SupervisorJob () // geannuleerd met activiteitenlevenscyclus
met (CoroutineScope (coroutineContext + supervisor)) {
  val something = launch (errorHandler) {
    ...
  }
}

En als u async gebruikt en in afwachting bent, pak dan altijd de try / catch in zoals hierboven beschreven, maar log zo nodig in.

d) Overweeg Resultaat / Fout Verzegelde Klasse

Overweeg een resultaat verzegelde klasse te gebruiken die een fout kan bevatten in plaats van uitzonderingen te maken:

verzegelde klasse Resultaat  {
  gegevensklasse succes (val-gegevens: T): resultaat ()
  gegevensklasse Fout (valfout: E): Resultaat ()
}

e) Naam Coroutine context

Wanneer je een async lambda declareert, kun je deze ook zo noemen:

async (CoroutineName ("MyCoroutine")) {}

Als u uw eigen thread maakt om in te voeren, kunt u deze ook een naam geven bij het maken van deze thread-uitvoerder:

newSingleThreadContext ( "MyCoroutineThread")

3. Executorpools en standaardpoolformaten

Coroutines is echt coöperatieve multitasking (met hulp van de compiler) op een beperkte poolgrootte. Dat betekent dat als u iets blokkeert in uw coroutine (bijvoorbeeld een blokkeer-API gebruikt), u de hele thread vastbindt totdat de blokkeerbewerking is voltooid. De coroutine wordt ook niet onderbroken, tenzij u een opbrengst of vertraging uitvoert, dus als u een lange verwerkingslus hebt, moet u controleren of de coroutine is geannuleerd (bel "sureActive ()" op de scope), zodat u vrij kunt komen de draad; dit is vergelijkbaar met hoe RxJava werkt.

Kotlin-coroutines hebben een aantal ingebouwde dispatchers (gelijk aan planners in RxJava). De hoofdverzender (als u niets opgeeft om op uit te voeren) is de gebruikersinterface; u moet alleen UI-elementen in deze context wijzigen. Er is ook een Dispatchers.Unconfined die tussen UI en achtergrondthreads kan springen, zodat deze niet op één thread staat; dit mag in het algemeen niet worden gebruikt, behalve bij unit-tests. Er is een Dispatchers.IO voor IO-afhandeling (netwerkoproepen die vaak worden onderbroken). Ten slotte is er een Dispatchers.Default die de belangrijkste achtergrondthreadpool is, maar deze is beperkt tot het aantal CPU's.

In de praktijk moet u een interface gebruiken voor algemene dispatchers die worden doorgegeven via de constructor van uw klasse, zodat u verschillende kunt verwisselen voor testen. bijv .:

interface CoroutineDispatchers {
  val UI: Dispatcher
  val IO: Dispatcher
  val Berekening: Dispatcher
  fun newThread (val name: String): Dispatcher
}

4. Vermijden van gegevensbeschadiging

Geen opschortende functies om gegevens buiten de functie te wijzigen. Dit kan bijvoorbeeld onbedoelde gegevenswijziging zijn als de twee methoden worden uitgevoerd vanuit verschillende threads:

val list = mutableListOf (1, 2)
opschorten leuke updateList1 () {
  list [0] = list [0] + 1
}
opschorten leuke updateList2 () {
  list.clear ()
}

U kunt dit soort problemen voorkomen door:
- uw coroutines een onveranderlijk object laten terugsturen in plaats van er een uit te reiken en te veranderen
- voer al deze coroutines uit in een context met één thread die is gemaakt via: newSingleThreadContext ("contextname")

5. Maak Proguard blij

Deze regels moeten worden toegevoegd voor release-builds van uw app:

-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembernames class kotlinx. ** {volatile ; }

6. Interop met Java

Als u aan een oudere app werkt, heeft u ongetwijfeld een aanzienlijk deel van de Java-code. U kunt coroutines uit Java bellen door een CompletableFuture te retourneren (vergeet niet het kotlinx-coroutines-jdk8-artefact op te nemen):

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

7. Retrofit niet nodig met context

Als u de Retrofit coroutines-adapter gebruikt, krijgt u een Uitgesteld die de asynchrone call van okhttp onder de motorkap gebruikt. U hoeft dus niet metContext (Dispatchers.IO) toe te voegen zoals u zou moeten doen met RxJava om ervoor te zorgen dat de code op een IO-thread draait; als u de Retrofit coroutines-adapter niet gebruikt en rechtstreeks een Retrofit-oproep belt, hebt u dit nodig metContext.

De Android Arch Components Room DB werkt ook automatisch in een niet-UI-context, dus u hebt geen behoefte aanContext.

Referenties:

  • 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