-
Notifications
You must be signed in to change notification settings - Fork 6.2k
Creating and Executing Async Tasks
Legacy note: This guide originally taught
AsyncTask, which was deprecated in API level 30 (Android 11). Its deprecation note reads: "Use the standardjava.util.concurrentor Kotlin concurrency utilities instead." This guide now teaches the recommended replacement for app code — Kotlin coroutines — and closes with a short summary of the legacyAsyncTaskcontract for anyone maintaining older code.
Long-running operations such as downloading data from an API, decoding an image, or reading from disk cannot run on the main (UI) thread: blocking the main thread freezes the interface and can trigger an "Application Not Responding" (ANR) dialog. Such work needs to run in the background, with the results delivered back to the main thread to update the UI.
Regardless of the tool used, a typical background task follows the same arc:
- Pre - Execute code on the UI thread before starting the task (e.g. show a progress indicator)
- Task - Run the work on a background thread given certain inputs (e.g. fetch data)
- Updates - Display progress updates during the task (optional)
- Post - Execute code on the UI thread following completion of the task (e.g. show data)
Kotlin coroutines are Android's recommended tool for this. A coroutine can suspend instead of blocking a thread, and switching between the main thread and a background thread pool is a single function call rather than a class with four callbacks.
Add the coroutines library and the lifecycle-aware coroutine scopes to your app/build.gradle dependencies (versions current as of this writing — check Maven Central and the AndroidX Lifecycle releases page for the latest):
dependencies {
// Coroutines support for Android (Dispatchers.Main, etc.)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"
// lifecycleScope for Activities and Fragments
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.11.0"
}Inside an Activity or Fragment, launch coroutines from the built-in lifecycleScope: "Any coroutine launched in this scope is canceled when the Lifecycle is destroyed," so a finished screen never receives stray UI updates. The coroutine body runs on the main thread; wrap the blocking work in withContext(Dispatchers.IO) to move just that part onto a background thread pool:
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initiate the background task
downloadImageAsync("https://www.gstatic.com/webp/gallery/1.jpg")
}
private fun downloadImageAsync(url: String) {
val progressBar = findViewById<ProgressBar>(R.id.progressBar)
val imageView = findViewById<ImageView>(R.id.imageView)
lifecycleScope.launch {
// 1. Pre: runs on the main thread before the work starts
progressBar.visibility = View.VISIBLE
// 2. Task: suspend while the download runs on the IO dispatcher
val bitmap: Bitmap = withContext(Dispatchers.IO) {
// downloadImageFromUrl is a plain blocking function, e.g. an
// HttpURLConnection or OkHttp call that decodes the response
downloadImageFromUrl(url)
}
// 4. Post: back on the main thread with the result
imageView.setImageBitmap(bitmap)
progressBar.visibility = View.INVISIBLE
}
}
}Note how the "pre", "task", and "post" steps read top-to-bottom in one function: launch starts the block on the main thread, withContext(Dispatchers.IO) suspends it while the download runs on a background thread, and execution resumes on the main thread with the Bitmap result in hand.
A dispatcher determines which thread pool a piece of coroutine code runs on:
-
Dispatchers.Main- The Android main thread. Use for UI updates and light work.lifecycleScope.launch { }uses this by default. -
Dispatchers.IO- A pool optimized for disk and network I/O (reading files, network calls, database queries). -
Dispatchers.Default- A pool optimized for CPU-intensive work (sorting large lists, parsing JSON, image processing).
The Android coroutines guide calls a function main-safe "when it doesn't block UI updates on the main thread." When writing your own suspend functions, wrap the blocking part in withContext inside the function itself so every caller gets main-safety for free.
Because code between suspension points runs on the main thread, progress updates need no special callback — split the work into chunks and update the UI between them:
private fun downloadFileInChunks(totalChunks: Int) {
val progressBar = findViewById<ProgressBar>(R.id.progressBar)
lifecycleScope.launch {
progressBar.progress = 0
progressBar.visibility = View.VISIBLE
for (chunkIndex in 1..totalChunks) {
// 2. Task: process one chunk off the main thread
withContext(Dispatchers.IO) {
// downloadChunk is a plain blocking function, like
// downloadImageFromUrl above
downloadChunk(chunkIndex)
}
// 3. Updates: back on the main thread between chunks
progressBar.progress = (chunkIndex * 100) / totalChunks
}
progressBar.visibility = View.INVISIBLE
}
}launch returns a Job handle that can be kept and cancelled — for example from a "Cancel" button:
class MainActivity : AppCompatActivity() {
private var downloadJob: Job? = null
private fun downloadImageAsync(url: String) {
val imageView = findViewById<ImageView>(R.id.imageView)
downloadJob = lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) { downloadImageFromUrl(url) }
imageView.setImageBitmap(bitmap)
}
}
private fun cancelDownload() {
// Stops the coroutine if it is still running
downloadJob?.cancel()
}
}Two things to keep in mind:
- Cancellation is cooperative: per the Kotlin cancellation docs, "coroutine code has to cooperate to be cancellable." A blocking call already in flight inside
withContextfinishes its current call; the coroutine stops at the next suspension point. - You rarely need to cancel by hand for lifecycle reasons —
lifecycleScopecancels its coroutines automatically when theActivityorFragmentis destroyed, which avoids the leaked-Activityproblem that plaguedAsyncTask.
A coroutine in lifecycleScope is cancelled when its screen is destroyed — including on a device rotation. For work that should continue across configuration changes, launch it from a ViewModel's viewModelScope instead. Per the Android coroutines guide: "If the ViewModel is destroyed because the user is navigating away from the screen, viewModelScope is automatically cancelled, and all running coroutines are canceled as well."
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ImageViewModel(private val repository: ImageRepository) : ViewModel() {
fun loadImage(url: String) {
viewModelScope.launch {
val bitmap = withContext(Dispatchers.IO) {
repository.downloadImage(url)
}
// Expose the result to the UI, e.g. through LiveData or StateFlow
}
}
}Coroutines live and die with your app's process. For background work that must run even if the user closes the app or reboots the device (periodic sync, log uploads), use WorkManager — see Repeating Periodic Tasks for a worked example — or a background service for user-visible long-running work.
Older codebases are full of AsyncTask subclasses, so it is worth recognizing the shape. An AsyncTask<Params, Progress, Result> declared three generic types (the input passed to execute(), the progress unit, and the background return type) and up to four callbacks, which map onto the coroutine pattern above:
AsyncTask callback |
Thread | Coroutine equivalent |
|---|---|---|
onPreExecute() |
Main | Code in launch { } before the first withContext
|
doInBackground(Params...) |
Background | The withContext(Dispatchers.IO) { } block; its last expression is the result |
onProgressUpdate(Progress...) |
Main | Main-thread code between background chunks (see "Reporting Progress") |
onPostExecute(Result) |
Main | Code in launch { } after withContext returns |
When migrating, also note what AsyncTask did not handle: the task kept running (and could leak its Activity) after a configuration change, which is exactly what lifecycleScope and viewModelScope fix.
Created by CodePath with much help from the community. Contributed content licensed under cc-wiki with attribution required. You are free to remix and reuse, as long as you attribute and use a similar license.
Finding these guides helpful?
We need help from the broader community to improve these guides, add new topics and keep the topics up-to-date. See our contribution guidelines here and our topic issues list for great ways to help out.
Check these same guides through our standalone viewer for a better browsing experience and an improved search. Follow us on twitter @codepath for access to more useful Android development resources.