Emmanuel Enya I am a computer engineering graduate with five years of professional experience building modern Android applications. I am a huge fan of clean code because clarity is King 😄

Moving away from Kotlin’s AsyncTask: Alternative solutions

5 min read 1646

Moving away from Kotlin's AsyncTask: Alternative solutions

In the world of Android, long-running operations — such as heavy computational calculations, database operations, or network operations — aren’t expected to be executed on the UI or main thread because these operations don’t complete immediately, and tend to block the thread from which they are called.

It would be a total UI disaster if these operations were done on the main thread. In modern apps, there’s a huge dependency on long-running operations to provide users with rich content. And, as we know, maintaining a great UI experience is very appealing and keeps users coming back. To avoid UI jank and freezes, the work should be lifted off the main thread and offloaded onto another thread to avoid losing users.

The Android framework has a strict policy on this; by default, it does not permit long-running operations like database interactions or network requests to be performed on the main thread. An exception is thrown at runtime, if this policy is violated.

Android originally designed the AsyncTask API to help with asynchronous tasks without needing a third-party library. I will not dwell on the internal workings of AsyncTask because, as of API level 30, AsyncTask was deprecated in favor of other alternatives, which we will discuss in this post.

Jump ahead:

Why was AsyncTask deprecated?

There are three main parts of this API that you should understand before working with this API:

  • AsyncTask has to be subclassed before you can use it
  • It comes with three generic types, which are Params, Progress, and Result
  • There are four steps to execution:
    • onPreExecute
    • doInBackground
    • onProgressUpdate
    • onPostExecute

Sample usage of AsyncTask

Let’s take a look at some code to start:

class DoWorkAsync(private val binding: ActivityMainBinding) :
    AsyncTask<Int, Unit, AsyncResult>() {

    override fun onPreExecute() {
        println("running onPreExecute on ${Thread.currentThread().name}")
        binding.displayText.text = "heavy calculation ongoing..."

    override fun doInBackground(vararg params: Int?): AsyncResult {
        println("running doInBackground on ${Thread.currentThread().name}")
        val param = params.first() ?: return AsyncResult.Error
        val factorial = factorial(param)
        return AsyncResult.Success(param, factorial)

    override fun onPostExecute(result: AsyncResult?) {
        println("running onPostExecute on ${Thread.currentThread().name}")
        when (result) {
            is AsyncResult.Success -> "Factorial of ${result.input} = ${result.value}"
            AsyncResult.Error, null -> "An error occurred while computing the value"
        }.also { binding.displayText.text = it }

In the code block above, I created a DoWorkAsync class that extends AsyncTask, and specified the generic types as Int, Unit, and AsyncResult. These types represents Params, Progress, and Result, respectively.

onPreExecute is invoked before we execute the long-running operations in doInBackground. This is where you can do things like show a progress bar to indicate that work is currently being done.

The actual task is done in doInBackground; the work done here is lifted out of the main thread to another thread to ensure a smooth user experience.

Accessing views via this method is not safe because you are attempting to access a view which was created in the main thread from another thread. Technically, the application should crash if this happens.

onPostExecute is called after the work is done in doInBackground. Accessing views from here is safe because we call from the main thread, where the views were initially created.

See the below screenshot for the thread name as printed to console.

The thread name is printed to the console

Using the AsyncTask API might allow you to execute asynchronous tasks, but there are issues in plain sight:

  1. The set up is too cumbersome for simple tasks
  2. The API is prone to memory leaks
  3. The API has been deprecated as of API level 30

Alternatives to the deprecated AsyncTask

Due to AsyncTask‘s shortcomings and deprecation, developers should turn to one of these alternative solutions for asynchronous programming in Android:

  1. Kotlin coroutines
  2. RxJava
  3. Executors

The first two are more popular and have simpler API usages, so we’ll focus on those in this post.

What are Kotlin coroutines?

Coroutines are an approach to concurrency where you can suspend and resume the execution of tasks without blocking the UI thread. Multiple coroutines can be executed on a single thread without blocking the thread.

Kotlin coroutines were developed from scratch in Kotlin by JetBrains to reduce the hassle when dealing with asynchronous tasks and concurrency. One of its perks is seen in the simplification of asynchronicity with sequential code.

To use Kotlin coroutines in Android, there are few basic components you should grasp.

Suspend functions

Suspend functions are similar to regular functions. The only difference is the suspend modifier on the function. At compile time, the compiler marks such function as a suspension point. These kind of functions can be suspended or resumed. One final point is that suspend functions can only be called from a coroutine or another suspend function.

A suspend function is defined like so:

suspend fun invokeSuspendFunc(){ }

And a regular function is defined like so:

fun invokeRegularFunc(){ }

Coroutine builders

Coroutine builders are use to launch new coroutines. The common builders are launch() and async().

Coroutine context and dispatchers

This determine which thread an operation should be executed.

Using Kotlin coroutines for async tasks

To get started with coroutines, we’ll add some extra dependencies to launch the coroutine from a logical scope within your app. The advantage of this is that, when the scope within which a coroutine is launched is destroyed, the coroutine is automatically canceled.

Copy the dependencies from the code block below.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"

For demonstration, I will stick to the same task we used previously, finding the factorial of a number.

More great articles from LogRocket:

private fun coDoWork(binding: ActivityMainBinding) {
    lifecycleScope.launch(Dispatchers.IO) {
        println("running task on ${Thread.currentThread().name}")
        val result = factorial(input)
        val text = "Factorial of $result = $result"
        withContext(Dispatchers.Main) {
            println("Accessing UI on ${Thread.currentThread().name}")
            binding.displayText.text = text
    println("running end on ${Thread.currentThread().name}")

Inspecting the output of the above code block, the statement in line 11 is executed before the code in the coroutine block. We’ve been able to achieve asynchronicity with coroutines using sequential code, without blocking the main thread.

In line 2, we create a coroutine and marked its execution to be done on a worker thread. This frees the main thread to execute other operations, like the statement in line 11.

Because UI applications are single-threaded, Kotlin coroutines gives us the flexibility to switch between threads to carry out operations that are thread-specific; one example is accessing UI components. binding.displayText.*text* = text will throw an exception if accessed from a thread that is not the main thread. To safely notify the UI of the result of the work done, we switch to the main thread using withContext(Dispatchers.Main) {}.

What is RxJava?

RxJava is a Java implementation of the reactiveX library, which is based on the reactive streams specification. The basic idea behind it is described by the observer pattern.

There are two main components of this paradigm: observable and observer. The observable is responsible for emitting a stream of data, which is consumed by an observer. To establish this connection, an observer has to subscribe to the observable.

Furthermore, the items emitted by an observable are subject to transformation and manipulation, depending on the use case. According to this list on their GitHub repo, RxJava has a ton of operators (over 200) that can be applied to a stream of data emitted before it gets delivered to the observer.

Enough talk about RxJava; let’s dive into some practical usage of RxJava for asynchronous programming.

Using RxJava for async tasks

Add the necessary Gradle dependencies like so:

implementation "io.reactivex.rxjava3:rxjava:3.1.5"
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'

Our objective here is to asynchronously calculate the factorial of a number and emit the result of the computation on the main thread, where it will be used.

First, we will set up our observable like so:

val observable = Observable.create<BigInteger> {

The above code block is fairly simple: first, we create an observable that emits the factorial of a number. subscribeOn specifies which scheduler the observable should use when an observer subscribes. In our case, we use Schedulers.io(), which is meant for I/O work in the background.

observeOn specifies which scheduler the observer should observe the observable. In our case, we’ve used AndroidSchedulers.mainThread() to indicate the main thread.

Implement the observer like so:

val observer = object : Observer<BigInteger> {
    override fun onSubscribe(disposable: Disposable) {

    override fun onNext(result: BigInteger) {
        val text = "Factorial of $result = $result"
        binding.displayText.text = text

    override fun onError(error: Throwable) {
        binding.displayText.text = error.message

    override fun onComplete() {
        println("operation completed")

The observer implementation is fairly straightforward. The callbacks are invoked at different times, depending on the signal from the observable.

The actual subscription is like so:


This is the last piece of the set up, and it establishes a connection between the observable and the observer; see it as a switch that closes a circuit. Only when when there is an active subscription will data be emitted to the observer.

For complete code sample, check my GitHub repo.


The deprecation of AsyncTask points further to its shortcomings on Android. Hence, taking advantage of the other alternatives we’ve discussed in this article is highly recommended. Both coroutines and RxJava are design to help with concurrency and asynchronous programming.

Kotlin coroutines’ sequential approach to asynchronous programming gives it an edge over RxJava in the Android community; however, there are specific use cases where each shine; coroutines stand out in making network requests and performing database operations, and on the flip side, RxJava is very powerful for handling form validations and implementing a reactive system.

LogRocket: Instantly recreate issues in your Android apps.

LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your Android apps — .

Emmanuel Enya I am a computer engineering graduate with five years of professional experience building modern Android applications. I am a huge fan of clean code because clarity is King 😄

Leave a Reply