Паттерны и антипаттерны корутин в Kotlin – Devcolibri

Паттерны и антипаттерны корутин в Kotlin

Паттерны и антипаттерны корутин в Kotlin

Решил написать о некоторых вещах, которых, по моему мнению, стоит и не стоит избегать при использовании корутин Kotlin.

Оборачивайте асинхронные вызовы в coroutineScope или используйте SupervisorJob для обработки исключений

❌ Если в блоке async может произойти исключение, не полагайтесь на блок try/catch.

val job: Job = Job()
val scope = CoroutineScope(Dispatchers.Default + job)

// may throw Exception
fun doWork(): Deferred<String> = scope.async { ... }   // (1)

fun loadData() = scope.launch {
    try {
        doWork().await()                               // (2)
    } catch (e: Exception) { ... }
}

В приведённом выше примере функция doWork запускает новую корутину (1), которая может выбросить необработанное исключение. Если вы попытаетесь обернуть doWork блоком try/catch (2), приложение всё равно упадёт.

Это происходит потому, что отказ любого дочернего компонента job приводит к немедленному отказу его родителя.

✅ Один из способов избежать ошибки — использовать SupervisorJob (1).

Сбой или отмена выполнения дочернего компонента не приведёт к сбою родителя и не повлияет на другие компоненты.

val job = SupervisorJob()                               // (1)
val scope = CoroutineScope(Dispatchers.Default + job)

// may throw Exception
fun doWork(): Deferred<String> = scope.async { ... }

fun loadData() = scope.launch {
    try {
        doWork().await()
    } catch (e: Exception) { ... }
}

Примечание: это будет работать, только если вы явно запустите свой асинхронный вызов в рамках корутины с SupervisorJob. Таким образом, приведённый ниже код всё равно приведёт к сбою вашего приложения, потому что async запускается в рамках родительской корутины (1).

val job = SupervisorJob()                               
val scope = CoroutineScope(Dispatchers.Default + job)

fun loadData() = scope.launch {
    try {
        async {                                         // (1)
            // may throw Exception 
        }.await()
    } catch (e: Exception) { ... }
}

✅ Другой способ избежать сбоя, который является более предпочтительным, заключается в том, чтобы обернуть async в coroutineScope (1). Теперь, когда исключение происходит внутри async, оно отменяет все другие корутины, созданные в этой области, не касаясь при этом внешней области. (2)

val job = SupervisorJob()                               
val scope = CoroutineScope(Dispatchers.Default + job)

// may throw Exception
fun doWork(): Deferred<String> = coroutineScope {     // (1)
    async { ... }
}

fun loadData() = scope.launch {                       // (2)
    try {
        doWork().await()
    } catch (e: Exception) { ... }
}

Кроме того, вы можете обрабатывать исключения внутри блока async.

Используйте главный диспетчер для корневых корутин

❌ Если вам нужно выполнить фоновую работу и обновить пользовательский интерфейс внутри своей корневой корутины, запускайте её с помощью главного диспетчера.

val scope = CoroutineScope(Dispatchers.Default)          // (1)

fun login() = scope.launch {
    withContext(Dispatcher.Main) { view.showLoading() }  // (2)  
    networkClient.login(...)
    withContext(Dispatcher.Main) { view.hideLoading() }  // (2)
}

В приведённом выше примере мы запускаем корневую корутину, используя в CoroutineScope диспетчер по умолчанию (1). При таком подходе каждый раз, когда нам нужно будет обновлять пользовательский интерфейс, мы будем должны переключать контекст (2).

✅ В большинстве случаев предпочтительнее создать CoroutineScope сразу с главным диспетчером, что приведёт к упрощению кода и менее явному переключению контекста.

val scope = CoroutineScope(Dispatchers.Main)

fun login() = scope.launch {
    view.showLoading()    
    withContext(Dispatcher.IO) { networkClient.login(...) }
    view.hideLoading()
}

Избегайте использования ненужных async/await

❌ Если вы используете функцию async и сразу же вызываете await, то вам следует прекратить это делать.

launch {
    val data = async(Dispatchers.Default) { /* code */ }.await()
}

✅ Если вы хотите переключить контекст корутины и немедленно приостановить родительскую корутину, то withContext — это самый предпочтительный для этого способ.

launch {
    val data = withContext(Dispatchers.Default) { /* code */ }
}

С точки зрения производительности это не такая большая проблема (даже если учесть, что async создаёт новую корутину для выполнения работы), но семантически async подразумевает, что вы хотите запустить несколько корутин в фоновом режиме и только потом ждать их.

Избегайте отмены job

❌ Если вам нужно отменить корутину, не отменяйте job.

class WorkManager {

    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    
    fun doWork1() {
        scope.launch { /* do work */ }
    }
    
    fun doWork2() {
        scope.launch { /* do work */ }
    }
    
    fun cancelAllWork() {
        job.cancel()
    }
}

fun main() {
    val workManager = WorkManager()

    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1() // (1)
}

Проблема с приведённым выше кодом заключается в том, что когда мы отменяем job, мы переводим его в завершённое состояние. Корутины, запущенные в рамках завершённого job, выполнены не будут (1).

✅ Если вы хотите отменить все корутины в определённой области, вы можете использовать функцию cancelChildren. Кроме того, хорошей практикой является предоставление возможности отмены отдельных job (2).

class WorkManager {

    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    
    fun doWork1(): Job = scope.launch { /* do work */ } // (2)
    
    fun doWork2(): Job = scope.launch { /* do work */ } // (2)
    
    fun cancelAllWork() {
        scope.coroutineContext.cancelChildren()         // (1)                             
    }
}

fun main() {
    val workManager = WorkManager()

    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1()
}

Избегайте написания функции приостановки, используя неявный диспетчер

❌ Не пишите функцию suspend, выполнение которой будет зависеть от определенного диспетчера корутин.

suspend fun login(): Result {
    view.showLoading()
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()

    return result
}

В приведённом выше примере функция входа в систему является функцией приостановки и она завершится сбоем, если вы запустите её из корутины, которая не будет использовать главный диспетчер.

launch(Dispatcher.Main) {     // (1) всё в порядке
    val loginResult = login()
    ...
}

launch(Dispatcher.Default) {  // (2) возникнет ошибка
    val loginResult = login()
    ...
}

CalledFromWrongThreadException: только исходный поток, создавший иерархию View-компонентов, имеет к ним доступ.

✅ Создайте свою функцию приостановки таким образом, чтобы её можно было выполнять из любого диспетчера корутин.

suspend fun login(): Result = withContext(Dispatcher.Main) {
    view.showLoading()
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()

return result
}

Теперь мы можем вызвать нашу функцию входа в систему из любого диспетчера.

launch(Dispatcher.Main) {     // (1) no crash
    val loginResult = login()
    ...
}

launch(Dispatcher.Default) {  // (2) no crash ether
    val loginResult = login()
    ...
}

Избегайте использования глобальной области видимости

❌ Если вы используете GlobalScope везде в своём Android-приложении, вам следует прекратить это делать.

GlobalScope.launch {
    // code
}

Глобальная область видимости используется для запуска корутин верхнего уровня, которые работают в течение всего времени жизни приложения и не отменяются раньше времени.

 

Код приложения обычно должен использовать определяемый приложением CoroutineScope, поэтому использование async или launch в GlobalScope крайне не рекомендуется.

✅ В Android корутина может быть легко ограничена жизненным циклом Activity, Fragment, View или ViewModel.

class MainActivity : AppCompatActivity(), CoroutineScope {

    private val job = SupervisorJob()
    
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job
    
    override fun onDestroy() {
        super.onDestroy()
        coroutineContext.cancelChildren()
    }
    
    fun loadData() = launch {
        // code
    }
}

Перевод статьи «Kotlin Coroutines patterns & anti-patterns»

ПОХОЖИЕ ПУБЛИКАЦИИ

3055
13/12/2018

0 комментариев к статье "Паттерны и антипаттерны корутин в Kotlin"

Добавить комментарий

Сайт использует cookie-файлы для того, чтобы вам было удобнее им пользоваться. Для продолжения работы с сайтом, вам необходимо принять использование cookie-файлов.

Я ознакомлен(а)