Давайте рассмотрим ситуацию, когда у нас есть вьюха, например ImageView, которую мы сначала должны подготовить перед отрисовкой — например, вычислить ее размеры, форму, или применить блюр-эффект и т.д. Эти вычисления могут стать дорогостоящей операцией, поэтому лучше перенести их в фоновый поток.
Деды-джависты создадут ранабл и потом при помощи хэндлера перенесут результат в основной поток и применят на вьюхе (первое, что приходит в голову).
Как это можно сделать быстро и удобно в котлине с его корутинами:
Для начала создадим kotlin-extension функцию:
Теперь смоделируем ситуацию: у нас есть RecyclerView, в каждом айтеме есть картинка. Перед показом мы хотим эту картинку заблюрить (размыть). Вот как это будет без асинхронщины:
Результат:
Как видно — потеря кадров существенная.
Теперь используем нашу функцию:
Результат:
Вся фишка в том, что мы привязываемся к жизненному циклу вью. Поэтому, когда вьюха открепляется от родителя, а это происходит в ресайклере постоянно при потере видимости айтема, или во фрагменте\активити при их уничтожении, то мы можем спокойно остановить и отменить корутину, зная, что результат выводить уже некуда, вьюха готова к уничтожению.
Чтобы было наглядно и понятно, рекомендую в вашем ВьюХолдере написать такой код и посмотреть логи:
И вы увидите, на каком ВьюХолдере какая корутина начинает работать, а на каком отменяется и прекращает работу.
Деды-джависты создадут ранабл и потом при помощи хэндлера перенесут результат в основной поток и применят на вьюхе (первое, что приходит в голову).
Как это можно сделать быстро и удобно в котлине с его корутинами:
Для начала создадим 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")
})
И вы увидите, на каком ВьюХолдере какая корутина начинает работать, а на каком отменяется и прекращает работу.