Working with View asynchronously using coroutine

Let's look at a situation where we have a view, for example, ImageView, which we must first prepare before rendering - for example, calculate its size, shape, or apply a bluer effect, etc. These calculations can be an expensive operation, so it’s better to transfer them to the background thread.

Javista grandfathers will create a runabl and then use the handler to transfer the result to the main stream and use it on the view (the first thing that comes to mind).

How can this be done quickly and conveniently in a cauldron with its coroutines:

First, create the kotlin-extension function:

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)
    }
}

Now we will simulate the situation: we have a RecyclerView, in each item there is a picture. Before the show, we want to blur this picture (blur). Here's how it would be without asynchronization:

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)
    }
}

Result:



As you can see, the loss of personnel is significant.

Now we use our function:

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)
        })
    }
}

Result:



The whole thing is that we are attached to the life cycle of the view. Therefore, when the view is detached from the parent, and this happens constantly in the recycler when the item is lost, or in the fragment / activity when they are destroyed, we can safely stop and cancel the coroutine, knowing that the result is nowhere to display, the view is ready for destruction.

To make it clear and understandable, I recommend writing such code in your ViewHolder and looking at the logs:

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")
})

And you will see on which Viewholder which coroutine starts working, and on which it is canceled and stops working.

All Articles