For a very long time in Android, if you needed to do anything asynchronously when making an app, you’d probably be using AsyncTask. AsyncTask is an API in Android’s framework that makes it easy(ish) to run operations in the background and return values when finished. And that makes sense. Unlike Kotlin’s Coroutines, AsyncTask has been around for a while, and it’s built right in.

However, both the design philosophy and implementation of AsyncTask have become somewhat outdated over the years. Because of that, Google has deprecated the AsyncTask API. You can still use it if you want, but Google doesn’t recommend doing so. Luckily, there are a whole bunch of alternatives to AsyncTask, including a feature of the Kotlin language — coroutines.

Kotlin’s coroutines API is an incredibly powerful framework which lets you do a whole bunch of things. This article is only going to scratch the surface of what’s possible. We’ll be going over the basics needed to migrate from AsyncTask to coroutines.

Adding Coroutines Support

Before you can start using coroutines, you need to actually add them to your project.

Adding Kotlin Support

If you already have Kotlin implemented, skip ahead to the next section. Otherwise, you’ll need to add Kotlin support to your project. Check out my tutorial on adding Kotlin to an existing project for more details.

Adding Coroutine Libraries

In your module-level build.gradle, include the following dependencies.

dependencies {
    ...
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
}

Sync your project, and Kotlin’s coroutines will now be available to use.

Using Coroutines

Implementing a CoroutineScope

In order to use coroutines, you’ll need to have a CoroutineScope instance available. An easy way to do this is to just implement it in your containing class.

For example, to implement a CoroutineScope in an Activity:

class SomeActivity : AppCompatActivity, CoroutineScope by MainScope() {
        ...         override fun onDestroy() {
            super.onDestroy()
     
           cancel()
        }
}

This will make SomeActivity implement the CoroutineScope interface by way of the MainScope class. MainScope will handle all implementation logic for CoroutineScope, while allowing you to use the CoroutineScope methods. Calling cancel() in onDestroy() makes sure that no asynchronous logic continues to run after the Activity exits.

Replacing AsyncTask with Coroutines

Say you have an AsyncTask inside an Activity that performs a long-running operation in the background and eventually returns a String. Something like the following.

private inner class SomeTask : AsyncTask<Void, Void, String>() {
    override fun doInBackground(vararg params: Void): String {
        try {
            //Pretend this is an actual operation that takes 10 seconds and not just sleeping.
            Thread.sleep(10000);
        } catch (e: InterruptedException) {}
         
        return "SomeString";
    }     override fun onPostExecute(result: String) {
        val someTextView = findViewById(R.id.some_text_view)
        someTextView.text = result
    }
}

Replacing this with a coroutine is easy. Just use the async() method. Kotlin’s async() runs on whichever Thread it was launched on, but does it asynchronously. This means you can update Views and such without having to worry about using the right Thread.

class SomeActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    ...     private fun doOperation() {
        async {
            //Inside coroutine scopes (like inside async here), delay is used instead of Thread.sleep.
            delay(10000)             val someTextView = findViewById(R.id.some_text_view)
            someTextView.text = "SomeString"
        }
    }
}

As you can see, using coroutines can be a lot simpler than using AsyncTask. You don’t have to just call async() and let it do its thing, though. You can hold a reference to it and even wait for it to finish.

val asyncJob = async {
    //Some operation
}
//Pause here until the async block is finished.
asyncJob.await() //This won't run until asyncJob finishes, but other operations started before the job, or started from another method, can still run.
doSomethingElse()

Returning values with async

You can even return a value from async() if you want. So the original example could become something like this.

class SomeActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    ...
    private fun doOperation() {
        val asyncJob = async {
            //Inside coroutine scopes (like inside async here), delay is used instead of Thread.sleep.
            delay(10000)             //Whatever the type is of the last line is what async() eventually returns.
           "SomeString"
        }         val result = asyncJob.await()         val someTextView = findViewById(R.id.some_text_view)
        someTextView.text = result
    }
}

Using withContext

For convenience, Kotlin provides withContext(). This inlines the whole await() thing and just returns the value to you.

class SomeActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    ...
    private fun doOperation() {
        //Run asynchronously on the main Thread.
        val result = withContext(Dispatchers.Main) {
            delay(10000)             "SomeResult"
        }         val someTextView = findViewById(R.id.some_text_view)
        someTextView.text = result }
}

Conclusion

The examples above are just some basic usage of Kotlin’s coroutines to get you started. You don’t have to limit coroutines to Activities or even anything with a proper lifecycle. You can run them basically anywhere. There are also more advanced operations, like choosing which Thread should run the asynchronous logic. This guide is mainly for showing how to replace a simple AsyncTask with a simple coroutine.

For more details on how coroutines work, and how you can make use of their more advanced features, check out the official Kotlin documentation.