Кращі практики Android Kotlin Coroutine

Це постійно підтримуваний набір найкращих практик використання Kotlin Coroutines на Android. Будь ласка, прокоментуйте нижче, якщо у вас є якісь пропозиції щодо чогось, що слід додати.

  1. Поводження з життєвими циклами Android

Аналогічним чином, як ви використовуєте CompositeDisposables з RxJava, Котлін Короутіни повинні бути скасовані в потрібний час, усвідомлюючи Android Livecycles з видами діяльності та фрагментами.

а) Використання 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 або додав завдання. Просто використовуйте "viewModelscope.launch {}". Зауважте, що 2.x означає, що ваш додаток повинен бути на AndroidX, оскільки я не впевнений, що вони планують підтримувати це в 1.x версії ViewModels.

b) Використання спостерігачів життєвого циклу

Цей інший метод створює область, яку ви додаєте до діяльності чи фрагменту (або будь-якого іншого, що реалізує життєвий цикл Android):

/ **
 * Контекст програми, який автоматично скасовується при знищенні інтерфейсу користувача
 * /
клас UiLifecycleScope: CoroutineScope, LifecycleObserver {

    приватна робота з латенітом: робота
    переосмислити val coroutineContext: CoroutineContext
        get () = робота + Dispatchers.Main

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

    @OnLifecycleEvent (Lifecycle.Event.ON_PAUSE)
    весело знищити () = job.cancel ()
}
... всередині діяльності або фрагменту підтримки Lib
приватний val uiScope = UiLifecycleScope ()
переосмислити забаву onCreate (збереженийInstanceState: пакет) {
  super.onCreate (збереженийInstanceState)
  lifecycle.addObserver (uiScope)
}

в) GlobalScope

Якщо ви використовуєте GlobalScope, це програма, яка триває термін служби програми. Ви використовуватимете це для синхронізації фону, оновлення репо тощо тощо (не пов'язаного з життєвим циклом діяльності).

г) Послуги

Служби можуть скасувати роботу в onDestroy:

приватна служба valJob = Робота ()
приватний val serviceScope = CoroutineScope (Dispatchers.Main + serviceJob)
перекрити забаву onCleared () {
 super.onCleared ()
 serviceJob.cancel ()
}

2. Поводження з винятками

a) У програмі async vs. запуск проти runBlocking

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

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

Виняток, закинутий у блок async {}, не поширюватиметься та не запускатиметься доти, доки цей блок не очікується, оскільки під ним дійсно є Java Deferred. Функція / метод виклику має містити винятки.

б) Вилов винятків

Якщо ви використовуєте async для запуску коду, який може кидати винятки, вам потрібно загортати код у coroutineScope, щоб правильно вилучити винятки (завдяки прикладу LouisC):

спробуйте {
    coroutineScope {
        val mayFailAsync1 = async {
            mayFail1 ()
        }
        val mayFailAsync2 = async {
            mayFail2 ()
        }
        useResult (mayFailAsync1.await (), mayFailAsync2.await ())
    }
} улов (e: IOException) {
    // впоратися з цим
    кинути MyIoException ("Помилка виконання IO", е)
} catch (e: AnotherException) {
    // впорайтеся і з цим
    кинути MyOtherException ("Помилка робити щось", е)
}

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

c) Винятки з реєстрації журналів

Якщо ви використовуєте GlobalScope.launch або актор, завжди передайте обробник винятків, який може реєструвати винятки. Наприклад

val errorHandler = CoroutineExceptionHandler {_, виняток ->
  // увійти до Crashlytics, logcat тощо.
}
val job = GlobalScope.launch (errorHandler) {
...
}

Майже завжди слід структурувати сфери застосування для Android, а обробник повинен використовуватися:

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

А якщо ви використовуєте асинхронізацію та очікуєте, завжди впишіться в спробу / ловити, як описано вище, але записуйте за потребою.

г) Розгляньте клас запечатаних результатами / помилками

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

Запечатаний клас Результат  {
  Клас даних Успіх (Val дані: T): Результат ()
  Помилка класу даних (помилка val: E): Результат ()
}

e) Ім'я контексту програми

Оголошуючи лямбда асинхронію, ви також можете її назвати так:

async (CoroutineName ("MyCoroutine")) {}

Якщо ви створюєте свій власний потік для запуску, ви можете також назвати його під час створення цього виконавця потоку:

newSingleThreadContext ("MyCoroutineThread")

3. Пули виконавців та розміри пулу за замовчуванням

Coroutines - це справді багатозадачність спільної роботи (за допомогою компілятора) з обмеженим розміром пулу потоків. Це означає, що якщо ви зробите щось блокування у своїй програмі (наприклад, використовуєте API блокування), ви зв’яжете всю нитку, поки не буде виконана операція блокування. Співпраця також не буде призупинено, якщо ви не зробите прибутковість чи затримку, тому якщо у вас довгий цикл обробки, не забудьте перевірити, чи було скасовано програму (зателефонуйте "увімкнено" () "у межах області), щоб ви могли звільнитись нитка; це схоже на те, як працює RxJava.

У котлін Котлін є кілька вбудованих диспетчерів (еквівалентно планувальникам в RxJava). Основний диспетчер (якщо ви не вказуєте нічого для запуску) - це інтерфейс користувача; слід змінювати лише елементи інтерфейсу в цьому контексті. Існує також Dispatchers.Unconfined, який може переходити між інтерфейсом користувача та фоновими потоками, щоб він не був на одній нитці; це, як правило, не слід використовувати, за винятком одиничних тестів. Існує Dispatchers.IO для обробки IO (мережеві дзвінки, які часто призупиняються). Нарешті, є Dispatchers.Default, який є основним пулом фонових потоків, але це обмежено кількістю процесорів.

На практиці ви повинні використовувати інтерфейс для загальних диспетчерів, які передаються через конструктор класу, щоб ви могли міняти різні для тестування. Наприклад:

інтерфейс CoroutineDispatchers {
  val UI: диспетчер
  val IO: диспетчер
  val Обчислення: диспетчер
  fun newThread (назва валу: String): диспетчер
}

4. Уникнення корупції даних

Не мають призупиняючих функцій змінювати дані поза функцією. Наприклад, це може мати ненавмисну ​​модифікацію даних, якщо два способи запускаються з різних потоків:

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

Ви можете уникнути подібного типу проблем:
- змусити ваші супроводи повернути незмінний предмет, а не досягати та змінювати його
- запустити всі ці підпрограми в одному потоковому контексті, створеному за допомогою: newSingleThreadContext ("ім'я контексту")

5. Зробіть Proguard щасливим

Слід додати правила для версій версії програми:

-клас імен класу kotlinx.coroutines.internal.MainDispatcherFactory {}
-клас-клас імен kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembernames class kotlinx. ** {volatile ; }

6. Інтероп з Java

Якщо ви працюєте над застарілим додатком, ви, без сумніву, матимете значну частину коду Java. Ви можете зателефонувати за допомогою супроводу Java, повернувши CompletableFuture (обов'язково включіть артефакт kotlinx-coroutines-jdk8):

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

7. Модернізація Не потрібно з контекстом

Якщо ви користуєтеся адаптером доопрацювання Retrofit, ви отримуєте Deferred, який використовує виклик асинхронізації okhttp під кришкою. Тож вам не потрібно додавати withContext (Dispatchers.IO) так, як вам це потрібно робити з RxJava, щоб переконатися, що код працює на потоці вводу-виводу; якщо ви не користуєтеся адаптером допрограми Retrofit і не дзвоните безпосередньо у виклик модернізації, вам знадобиться withContext.

База даних DB для компонентів арки Android також автоматично працює в контексті, що не є інтерфейсом користувача, тому вам не потрібно з 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