Skip to content

Creating and Executing Async Tasks

Mitchell Bryson edited this page Aug 6, 2026 · 19 revisions

Overview

Legacy note: This guide originally taught AsyncTask, which was deprecated in API level 30 (Android 11). Its deprecation note reads: "Use the standard java.util.concurrent or Kotlin concurrency utilities instead." This guide now teaches the recommended replacement for app code — Kotlin coroutines — and closes with a short summary of the legacy AsyncTask contract 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:

  1. Pre - Execute code on the UI thread before starting the task (e.g. show a progress indicator)
  2. Task - Run the work on a background thread given certain inputs (e.g. fetch data)
  3. Updates - Display progress updates during the task (optional)
  4. 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.

Setup

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"
}

Launching a Coroutine

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.

Understanding Dispatchers

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.

Reporting Progress

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
    }
}

Cancelling a Task

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 withContext finishes its current call; the coroutine stops at the next suspension point.
  • You rarely need to cancel by hand for lifecycle reasons — lifecycleScope cancels its coroutines automatically when the Activity or Fragment is destroyed, which avoids the leaked-Activity problem that plagued AsyncTask.

Surviving Configuration Changes with a ViewModel

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
        }
    }
}

Longer-Running and Deferrable Work

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.

Legacy: The AsyncTask Contract

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.

References

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.

Clone this wiki locally