Android Kotlin Coroutine Най-добри практики

Това е непрекъснато поддържан набор от най-добри практики за използване на Kotlin Coroutines на Android. Моля, коментирайте по-долу, ако имате предложения за нещо, което трябва да се добави.

  1. Работа с жизнените цикли на Android

По подобен начин, по който използвате CompositeDisposables с RxJava, Kotlin Coroutines трябва да бъде отменен в точното време с осведоменост за Android Livecycles с дейности и фрагменти.

a) Използване на Android Viewmodels

Това е най-лесният начин за настройка на процедурите, така че те да бъдат затворени в точното време, но той работи само в Android ViewModel, който има функция onCleared, която задачите на корута може да бъде надеждно отменена от:

частен изглед valModelJob = работа ()
частен val uiScope = CoroutineScope (Dispatchers.Main + viewModelJob)
замени забавлението onCleared () {
 super.onCleared ()
 uiScope.coroutineContext.cancelChildren ()
}

Забележка: От ViewModels 2.1.0-alpha01 това вече не е необходимо. Вече не е необходимо вашият зрителен модел да внедрява CoroutineScope, onCleared или да добавяте Job. Просто използвайте „viewModelscope.launch {}”. Обърнете внимание, че 2.x означава, че приложението ви ще трябва да бъде на AndroidX, тъй като не съм сигурен, че планират да го съобщят на 1.x версията на ViewModels.

б) Използване на наблюдатели на жизнения цикъл

Тази друга техника създава обхват, който прикачвате към дейност или фрагмент (или нещо друго, което реализира Android жизнен цикъл):

/ **
 * Контекст на Coroutine, който автоматично се анулира при унищожаване на потребителския интерфейс
 * /
клас UiLifecycleScope: CoroutineScope, LifecycleObserver {

    частна работа с латейн: работа
    отмени val coroutineContext: CoroutineContext
        get () = работа + Dispatchers.Main

    @OnLifecycleEvent (Lifecycle.Event.ON_START)
    забавно onCreate () {
        работа = работа ()
    }

    @OnLifecycleEvent (Lifecycle.Event.ON_PAUSE)
    забавно унищожи () = job.cancel ()
}
... вътре Дейност или фрагмент за поддръжка на либ
частен val uiScope = UiLifecycleScope ()
замени забавлението onCreate (saveInstanceState: bundle) {
  super.onCreate (savedInstanceState)
  lifecycle.addObserver (uiScope)
}

в) GlobalScope

Ако използвате GlobalScope, това е обхват, който продължава живота на приложението. Ще използвате това за синхронизация на фона, опресняване на репо и т.н. (не е обвързан с жизнения цикъл на дейността).

г) Услуги

Услугите могат да прекратят заданията си в onDestroy:

частна услуга val = Job ()
private val serviceScope = CoroutineScope (Dispatchers.Main + serviceJob)
замени забавлението onCleared () {
 super.onCleared ()
 serviceJob.cancel ()
}

2. Работа с изключения

а) В async срещу стартиране срещу runBlocking

Важно е да се отбележи, че изключенията в стартиращ блок {} ще сринат приложението без обработка на изключения. Винаги настройте обработващ изключение по подразбиране, който да премине като параметър за стартиране.

Изключение в блок runBlocking {} ще срине приложението, освен ако не добавите опит за улов. Винаги добавяйте пробване / улов, ако използвате runBlocking. В идеалния случай използвайте runBlocking само за тестове на единица.

Изключение, хвърлено в блок async {}, няма да се разпространява или изпълнява, докато блокът се изчака, защото наистина е отложен от Java под него. Функцията / методът на повикване трябва да улавя изключения.

б) Улов на изключения

Ако използвате async за стартиране на код, който може да хвърля изключения, трябва да увиете кода в coroutineScope, за да улавяте изключения правилно (благодарение на LouisC за примера):

опитвам {
    coroutineScope {
        val mayFailAsync1 = async {
            mayFail1 ()
        }
        val mayFailAsync2 = async {
            mayFail2 ()
        }
        useResult (mayFailAsync1.await (), mayFailAsync2.await ())
    }
} улов (д: IOException) {
    // справи се с това
    хвърли MyIoException ("Грешка при IO", д)
} улов (e: AnotherException) {
    // справете се и с това
    хвърлете MyOtherException ("Грешка при правенето на нещо", д)
}

Когато хванете изключението, увийте го в друго изключение (подобно на това, което правите за RxJava), така че да получите линията на стек-трасите в собствения си код, вместо да виждате стек-трак само с код на корута.

в) Изключения от регистрирането

Ако използвате GlobalScope.launch или участник, винаги предавайте обработващ изключения, който може да регистрира изключения. Например

val errorHandler = CoroutineExceptionHandler {_, изключение ->
  // влезте в Crashlytics, logcat и т.н.
}
val job = GlobalScope.launch (errorHandler) {
...
}

Почти винаги трябва да структурирате обхвата на Android и да се използва манипулатор:

val errorHandler = CoroutineExceptionHandler {_, изключение ->
  // влезте в Crashlytics, logcat и др .; може да бъде инжектирана зависимост
}
val supervisor = SupervisorJob () // отменен w / жизнен цикъл на дейността
с (CoroutineScope (coroutineContext + supervisor)) {
  val нещо = стартиране (errorHandler) {
    ...
  }
}

И ако използвате асинхронизация и чакате, винаги се обвивайте в опита / улавянето, както е описано по-горе, но регистрирайте според нуждите.

г) Помислете запечатан с резултат / грешка клас

Помислете да използвате клас запечатан с резултат, който може да съдържа грешка, вместо да хвърля изключения:

запечатан клас Резултат  {
  клас успех на данните (val данни: T): Резултат ()
  Грешка в данни данни Грешка (val грешка: E): Резултат ()
}

д) Име на контекста на процедурата

Когато декларирате асинхронна ламбда, можете също да я наречете така:

async (CoroutineName ("MyCoroutine")) {}

Ако създавате своя собствена тема, в която да се изпълнявате, можете също да я назовите, когато създавате този изпълнител на нишка:

newSingleThreadContext ( "MyCoroutineThread")

3. Пулове на изпълнители и Размери на пул по подразбиране

Coroutines е наистина съвместна многозадачност (с помощта на компилатор) при ограничен размер на нивата на нишката. Това означава, че ако направите нещо блокиране във вашата програма (напр. Използвате блокиращ API), ще завържете цялата нишка, докато не бъде извършена операцията по блокиране. Съвместимостта също няма да спре, освен ако не направите добив или закъснение, така че ако имате дълъг цикъл за обработка, не забравяйте да проверите дали коректът е анулиран (извикайте „sureActive ()“ в обхвата), за да можете да освободите нишката; това е подобно на това как работи RxJava.

Котлинът на Котлин има няколко вградени диспечери (еквивалентни на планиращите в RxJava). Основният диспечер (ако не посочите нещо, с което да стартирате) е потребителският интерфейс; трябва да промените UI елементи само в този контекст. Има и Dispatchers.Unconfined, който може да скача между потребителския интерфейс и фоновите нишки, така че да не е на една нишка; това обикновено не трябва да се използва освен в единични тестове. Има Dispatchers.IO за работа с IO (мрежови повиквания, които се спират често). И накрая, има Dispatchers.Default, който е основният пул от фонови нишки, но това е ограничено до броя на процесорите.

На практика трябва да използвате интерфейс за общи диспечери, които се предават чрез конструктора на вашия клас, за да можете да разменяте различни за тестване. Напр .:

интерфейс CoroutineDispatchers {
  val UI: Диспечер
  val IO: Диспечер
  val Изчисление: Диспечер
  fun newThread (име на val: String): Диспечер
}

4. Избягване на корупция на данни

Не разполагате със спиращи функции, променящи данните извън функцията. Например, това може да има непредвидена промяна на данни, ако двата метода се изпълняват от различни нишки:

val list = mutableListOf (1, 2)
спрете забавната актуализацияList1 () {
  list [0] = списък [0] + 1
}
спрете забавното актуализиранеList2 () {
  list.clear ()
}

Можете да избегнете този тип проблеми чрез:
- да накарате вашите съпротивления да върнат неизменен предмет, вместо да протегнете ръка и да го промените
- стартирайте всички тези процедури в един контекст с нишка, създаден чрез: newSingleThreadContext („име на контекста“)

5. Направете Proguard щастлив

Това трябва да се добавят правила за версии на приложението на вашето приложение:

-клас клас клас kotlinx.coroutines.internal.MainDispatcherFactory {}
-клас клас клас kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembernames клас kotlinx. ** {летливи <полета>; }

6. Interop с Java

Ако работите върху наследено приложение, без съмнение ще имате значителна част от Java кода. Можете да извикате съпротивления от Java, като върнете CompletableFuture (не забравяйте да включите kotlinx-coroutines-jdk8 артефакт):

doSomethingAsync (): CompletableFuture <Списък > =
   GlobalScope.future {doSomething ()}

7. Модернизиране Не е необходимо с Context

Ако използвате адаптора за модернизиране на Retrofit, получавате отложено, което използва асинхронното обаждане на okhttp под капака. Така че не е необходимо да добавяте withContext (Dispatchers.IO), както би трябвало да правите с RxJava, за да сте сигурни, че кодът работи на IO нишка; ако не използвате адаптора за модернизиране на Retrofit и се обадите директно на повикване за модернизация, се нуждаете от withContext.

Базата данни за компонент на Android Arch Components Room също работи автоматично в не-потребителски интерфейс, така че не е необходимо сContext.

Препратки:

  • 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