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:
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 itParams
, Progress
, and Result
onPreExecute
doInBackground
onProgressUpdate
onPostExecute
AsyncTask
Let’s take a look at some code to start:
class DoWorkAsync(private val binding: ActivityMainBinding) : AsyncTask<Int, Unit, AsyncResult>() { override fun onPreExecute() { super.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}") super.onPostExecute(result) 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.
Using the AsyncTask
API might allow you to execute asynchronous tasks, but there are issues in plain sight:
AsyncTask
Due to AsyncTask
‘s shortcomings and deprecation, developers should turn to one of these alternative solutions for asynchronous programming in Android:
The first two are more popular and have simpler API usages, so we’ll focus on those in this post.
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 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 are use to launch new coroutines. The common builders are launch()
and async()
.
This determine which thread an operation should be executed.
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.
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)
{}
.
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.
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> { it.onNext(factorial(input)) }.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())
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) { disposableContainer.add(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:
observable.subscribe(observer)
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 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 — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
One Reply to "Moving away from Kotlin’s AsyncTask: Alternative solutions"
You mentioned Executors in the Alternatives section, but said you were going to focus on the first two. Well, you did, but you completely forgot to mention Executors again at all. It would be nice if you could update the post to talk about Executors even a little.