Давайте рассмотрим ситуацию, когда у нас есть вьюха, например ImageView, которую мы сначала должны подготовить перед отрисовкой — например, вычислить ее размеры, форму, или применить блюр-эффект и т.д. Эти вычисления могут стать дорогостоящей операцией, поэтому лучше перенести их в фоновый поток.

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

Как это можно сделать быстро и удобно в котлине с его корутинами:

Для начала создадим kotlin-extension функцию:

inline fun <T> View.doAsync(
        crossinline backgroundTask: (scope: CoroutineScope) -> T, 
        crossinline result: (T?) -> Unit) {
    val job = CoroutineScope(Dispatchers.Main)
    // Добавляем слушатель, который будет отменять 
    // корутину, если вьюха откреплена
    val attachListener = object : View.OnAttachStateChangeListener {
        override fun onViewAttachedToWindow(p0: View?) {}
        override fun onViewDetachedFromWindow(p0: View?) {
            job.cancel()
            removeOnAttachStateChangeListener(this)
        }
    }
    this.addOnAttachStateChangeListener(attachListener)
    // Создаем Job, которая будет работать в основном потоке
    job.launch {
        // Создаем Deferred с результатом в фоновом потоке
        val data = async(Dispatchers.Default) {
            try {
                backgroundTask(this)
            } catch (e: Exception) {
                e.printStackTrace()
                return@async null
            }
        }
        if (isActive) {
            try {
                result.invoke(data.await())
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        // Отписываем слушатель по окончании Job
        this@doAsync.removeOnAttachStateChangeListener(attachListener)
    }
}

Теперь смоделируем ситуацию: у нас есть RecyclerView, в каждом айтеме есть картинка. Перед показом мы хотим эту картинку заблюрить (размыть). Вот как это будет без асинхронщины:

inner class PostHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val ivTest = itemView.iv_test
    fun bind() {
        val bitmap = ...ваш битмап
        val blurBitmap = bitmap?.addBlurShadow(Color.CYAN, 50.dp, 50.dp)
        ivTest.setImageBitmap(blurBitmap)
    }
}

Результат:



Как видно — потеря кадров существенная.

Теперь используем нашу функцию:

inner class PostHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val ivTest = itemView.iv_test
    fun bind() {
        val bitmap = ...ваш битмап
        itemView.doAsync({ scope -> 
            // В этой лямбде выполняем задачу в фоновом потоке
            return@doAsync bitmap?.addBlurShadow(Color.CYAN, 50.dp, 50.dp)
        }, { it ->
            // А в этой получаем готовый результат в виде битмапа в главном потоке
            ivTest.setImageBitmap(it)
        })
    }
}

Результат:



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

Чтобы было наглядно и понятно, рекомендую в вашем ВьюХолдере написать такой код и посмотреть логи:

itemView.doAsync({ scope ->
    logInfo("coroutine start")
    var x = 0
    // Не забывайте во время длительных операций проверять scope.isActive
    // и выполнять ваш код только если isActive = true, иначе корутина так и будет
    // крутиться в фоновом потоке, пока весь код не отработает
    while (x < 100 && scope.isActive) {
        TimeUnit.MILLISECONDS.sleep(100)
        logInfo("coroutine, position: $adapterPosition ${x++}")
    }
    logInfo("coroutine end")
}, {
    logInfo("coroutine DONE")
})

И вы увидите, на каком ВьюХолдере какая корутина начинает работать, а на каком отменяется и прекращает работу.