Search code examples
androidandroid-asynctaskandroid-lifecyclekotlin-coroutinesactivity-lifecycle

How to cancel with and without interruption using Coroutines on Android, including auto-cancellation according to lifecycle?


Background

I'm having problems in migrating from the simple (deprecated) AsyncTask and Executors to Kotlin Coroutines on Android

The problem

I can't find how I can perform the basic things I could have done on AsyncTask and even on Executors using Kotlin Coroutines.

In the past, I could choose to cancel a task with and without thread interruption. Now for some reason, given a task that I create on Coroutines, it's only without interruption, which means that if I run some code that has even "sleep" in it (not always by me), it won't be interrupted.

I also remember I was told somewhere that Coroutines is very nice on Android, as it automatically cancel all tasks if you are in the Activity. I couldn't find an explanation of how to do it though.

What I've tried and found

For the Coroutines task (called Deferred according to what I see) I think I've read that when I create it, I have to choose which cancellation it will support, and that for some reason I can't have them both. Not sure if this is true, but I still wanted to find out, as I want to have both for best migration. Using AsyncTask, I used to add them to a set (and remove when cancelled) so that upon Activity being finished, I could go over all and cancel them all. I even made a nice class to do it for me.

This is what I've created to test this:

class MainActivity : AppCompatActivity() {
    val uiScope = CoroutineScope(Dispatchers.Main)
    val bgDispatcher: CoroutineDispatcher = Dispatchers.IO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        loadData()
    }

    private fun loadData(): Job = uiScope.launch {
        Log.d("AppLog", "loadData")
        val task = async(bgDispatcher) {
            Log.d("AppLog", "bg start")
            try {
                Thread.sleep(5000L) //this could be any executing of code, including things not editable
            } catch (e: Exception) {
                Log.d("AppLog", "$e")
            }
            Log.d("AppLog", "bg done this.isActive?${this.isActive}")
            return@async 123
        }
        //simulation of cancellation for any reason, sadly without the ability to cancel with interruption
        Handler(mainLooper).postDelayed({
            task.cancel()
        }, 2000L)
        val result: Int = task.await()
        Log.d("AppLog", "got result:$result") // this is called even if you change orientation, which I might not want when done in Activity
    }
}

build gradle file:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"

The questions

  1. Is it possible on Coroutines to have a task that I can cancel with and without thread interruption?
  2. What should be added to make this work, so that when the Activity dies (orientation change, for example), it would be auto-cancelled (with the choice of with or without interruption) ? I guess I could use a similar solution as I had for AsyncTask, but I remember I was told there is a nice way to do it for Coroutines too.

Solution

  • By default, coroutines do not do thread interrupts - as per the Making computation code cancellable documentation, using yield() or checking isActive allows coroutine aware code to participate in cancellation.

    However, when interfacing with blocking code where you do want a thread interrupt, this is precisely the use case for runInterruptible(), which will cause the contained code to be thread interrupted when the coroutine scope is cancelled.

    This works perfectly with lifecycle-aware coroutine scopes, which automatically cancel when the Lifecycle is destroyed:

    class MainActivity : AppCompatActivity() {
        val bgDispatcher: CoroutineDispatcher = Dispatchers.IO
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            loadData()
        }
    
        private fun loadData(): Job = lifecycleScope.launch {
            Log.d("AppLog", "loadData")
            val result = runInterruptible(bgDispatcher) {
                Log.d("AppLog", "bg start")
                try {
                    Thread.sleep(5000L) //this could be any executing of code, including things not editable
                } catch (e: Exception) {
                    Log.d("AppLog", "$e")
                }
                Log.d("AppLog", "bg done this.isActive?${this.isActive}")
                return@runInterruptible 123
            }
            Log.d("AppLog", "got result:$result")
        }
    }