Android Threading Basics


Android Threading Basics Interview with follow-up questions

1. What is threading in Android and why is it important?

Android runs all UI work on a single main thread (also called the UI thread). If that thread is blocked for more than 5 seconds, the system triggers an ANR (Application Not Responding) dialog. Threading is how you keep the main thread free for rendering and input.

The main thread constraint

Only the main thread can update the UI. Any work that might take time — network requests, database reads, file I/O, complex computation — must run on a background thread, and results must be marshaled back to the main thread to update views.

Modern approach: Kotlin coroutines

Kotlin coroutines are the standard threading solution for Android in 2026. They provide structured concurrency with explicit scopes that tie background work to the lifecycle of a component:

  • viewModelScope in a ViewModel — cancelled when the ViewModel is cleared
  • lifecycleScope in an Activity or Fragment — cancelled when the lifecycle is destroyed
  • Dispatchers.IO for I/O-bound work (network, disk)
  • Dispatchers.Default for CPU-intensive work (sorting, parsing)
  • Dispatchers.Main to update UI
viewModelScope.launch {
    val result = withContext(Dispatchers.IO) { repository.fetchData() }
    _uiState.value = result
}

Flow for reactive streams

StateFlow and SharedFlow let you emit a stream of values across threads. The UI layer collects from a StateFlow on the main thread using repeatOnLifecycle, which automatically pauses collection when the app is in the background.

Why interviewers care

Threading questions often test whether you understand:

  • The ANR threshold and what causes it
  • The difference between launch and async in coroutines
  • How structured concurrency prevents leaked coroutines
  • When to use Dispatchers.IO vs Dispatchers.Default
  • How to safely update UI state from a background coroutine

Older mechanisms like AsyncTask (deprecated and removed), HandlerThread, and raw Thread still exist but should not be used in new code. Knowing why they were replaced and what replaced them shows depth.

↑ Back to top

Follow-up 1

Can you explain the concept of a main thread in Android?

In Android, the main thread is also known as the UI thread. It is responsible for handling user interactions, updating the user interface, and processing events. All UI-related operations, such as drawing views, responding to user input, and updating UI elements, must be performed on the main thread. This is because the main thread is the only thread that has access to the UI toolkit.

Follow-up 2

What are the potential issues if we perform a long operation on the main thread?

Performing a long operation on the main thread can lead to several issues. Firstly, it can cause the UI to become unresponsive, resulting in a poor user experience. If the main thread is blocked for an extended period, the app may even trigger an Application Not Responding (ANR) error and be terminated by the system. Additionally, performing long operations on the main thread can lead to dropped frames, choppy animations, and sluggish UI performance.

Follow-up 3

How can we avoid blocking the main thread?

To avoid blocking the main thread, we can offload time-consuming operations to background threads. This can be achieved by using various threading mechanisms provided by the Android framework, such as AsyncTask, HandlerThread, or Java's Executor framework. By executing these operations on separate threads, we ensure that the main thread remains free to handle user interactions and update the UI. Once the background operation is complete, we can communicate the results back to the main thread for UI updates if necessary.

2. What is AsyncTask in Android?

AsyncTask was a helper class that let you run code on a background thread and publish results to the main thread. It has been deprecated since Android API 30 (Android 11) and removed from the Android API surface entirely. You should never use it in new code, and existing code using it should be migrated.

Why it was deprecated

AsyncTask had fundamental design flaws:

  • It held an implicit reference to the outer Activity, which caused memory leaks and crashes when the Activity was destroyed before the task completed (e.g., on rotation).
  • It did not respect the Android lifecycle, so tasks continued running even after the UI that launched them was gone.
  • Its serial default executor (since API 11) meant tasks queued behind each other, causing unexpected delays.
  • Error handling was awkward — there was no built-in way to propagate exceptions to the UI thread.

The replacement: Kotlin coroutines

The modern equivalent is a coroutine launched in viewModelScope:

// ViewModel
fun loadData() {
    viewModelScope.launch {
        val result = withContext(Dispatchers.IO) {
            repository.fetchData()
        }
        _uiState.value = result
    }
}

This is lifecycle-aware (cancelled when the ViewModel is cleared), handles exceptions cleanly with try/catch, and doesn't leak the Activity.

Interview framing

If asked about AsyncTask in an interview, acknowledge it by name, explain that it is deprecated and why, then pivot to coroutines as the correct modern answer. Interviewers asking this in 2026 are testing whether you know the ecosystem has moved on. Describing AsyncTask as a current solution is a red flag.

The pattern interviewers are really probing is: how do you run background work safely with proper lifecycle cancellation? The answer is viewModelScope.launch with Dispatchers.IO.

↑ Back to top

Follow-up 1

Can you explain the lifecycle of an AsyncTask?

The lifecycle of an AsyncTask consists of four main steps:

  1. onPreExecute(): This method is called on the UI thread before the background task is executed. It is typically used to set up the task, such as showing a progress dialog.

  2. doInBackground(Params...): This method is called on a background thread and performs the actual work. It receives input parameters and returns a result.

  3. onProgressUpdate(Progress...): This method is called on the UI thread during the background task to update the progress. It can be used to update a progress bar or display intermediate results.

  4. onPostExecute(Result): This method is called on the UI thread after the background task is completed. It receives the result from the background task and can update the UI with the final result.

Follow-up 2

What are some common use cases for AsyncTask?

Some common use cases for AsyncTask include:

  1. Loading data from a remote server: AsyncTask can be used to fetch data from a remote server in the background and update the UI with the results.

  2. Performing network operations: AsyncTask can be used to perform network operations such as downloading files or making HTTP requests.

  3. Updating the UI with periodic updates: AsyncTask can be used to periodically update the UI with data from a background task, such as updating a progress bar or displaying real-time data.

  4. Performing database operations: AsyncTask can be used to perform database operations in the background, such as inserting or querying data from a SQLite database.

Follow-up 3

What are the potential issues with using AsyncTask?

There are a few potential issues with using AsyncTask:

  1. Memory leaks: If an AsyncTask is not properly cancelled or if it holds a reference to a long-lived object, it can cause memory leaks.

  2. Configuration changes: AsyncTask is tied to the lifecycle of the Activity or Fragment that creates it. If a configuration change (such as screen rotation) occurs while the AsyncTask is running, it can lead to crashes or unexpected behavior.

  3. Limited control over execution: AsyncTask provides limited control over the execution of background tasks. For more complex threading scenarios, other alternatives like ThreadPoolExecutor or RxJava may be more suitable.

  4. UI thread dependency: AsyncTask is tightly coupled with the UI thread, which means that it may not be suitable for long-running tasks or tasks that require frequent UI updates. In such cases, other approaches like using a Service or IntentService may be more appropriate.

3. How does the Handler class in Android work?

Handler is a low-level mechanism for sending messages and Runnable objects to a thread's MessageQueue. It is tied to a Looper, which is the loop that drains the queue one message at a time on a specific thread.

How it works

Every thread that calls Looper.prepare() and Looper.loop() gets a message queue. The main thread has a Looper by default. When you create a Handler without arguments (or with Looper.getMainLooper()), messages posted to it are dispatched on the main thread:

private val mainHandler = Handler(Looper.getMainLooper())

// Post a Runnable to run on the main thread after a delay
mainHandler.postDelayed({ updateUI() }, 1000L)

For a background thread, you typically use HandlerThread, which sets up its own Looper:

val handlerThread = HandlerThread("WorkerThread")
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper)
backgroundHandler.post { doBackgroundWork() }

When Handler is still relevant in 2026

Handler remains in use in specific scenarios:

  • Choreographer and rendering: The framework itself uses Handler/Looper heavily for the render pipeline. Choreographer.postFrameCallback is a Handler-based API used in custom animation and perf monitoring.
  • Legacy view system callbacks: Some view operations (e.g., measuring after layout) require posting a Runnable to defer execution to the next frame.
  • HandlerThread for serial background work: Still reasonable for cases like a serial queue for Bluetooth or serial port communication where you need strict ordering without coroutines.

Handler vs coroutines

For most application-level code, Kotlin coroutines have replaced Handler-based patterns. viewModelScope.launch with Dispatchers.Main or withContext(Dispatchers.Main) is cleaner, testable, and lifecycle-aware. Handler doesn't provide structured concurrency or cancellation.

Common interview gotchas

  • Memory leaks: An anonymous inner class Handler in an Activity holds an implicit reference to the Activity. The classic fix is to use a WeakReference to the Activity, or better — move to coroutines.
  • Handler vs post: view.post(runnable) is essentially syntactic sugar over posting to the main Handler; it also waits until the view is attached.
  • Looper.getMainLooper() vs no-arg constructor: The no-arg Handler() constructor was deprecated in API 30 because it silently attached to whatever thread's looper was current, causing subtle bugs. Always pass Looper explicitly.
↑ Back to top

Follow-up 1

What are some common use cases for Handlers?

Some common use cases for Handlers in Android include:

  1. Updating the UI from a background thread: Handlers can be used to post runnable objects to the main thread's message queue, allowing you to update the UI from a background thread.

  2. Delayed execution of tasks: Handlers can be used to schedule tasks to be executed after a certain delay, using the postDelayed() method.

  3. Periodic execution of tasks: Handlers can be used to schedule tasks to be executed periodically, using the postDelayed() method in combination with a recursive call.

  4. Communication between threads: Handlers can be used to send messages between different threads, allowing for inter-thread communication and synchronization.

Follow-up 2

How does a Handler relate to a Looper and a MessageQueue?

In Android, a Handler is associated with a Looper and a MessageQueue.

A Looper is responsible for managing the message queue of a thread. It continuously loops through the message queue, processing each message in the queue one by one. The Looper ensures that the thread stays alive as long as there are messages in the queue.

A MessageQueue is a queue that holds the messages to be processed by a Looper. It is implemented as a linked list of Message objects. When a Handler sends a message or posts a runnable object, it is added to the message queue of the associated Looper.

When a message is added to the message queue, the Looper retrieves the message from the queue and dispatches it to the appropriate Handler for processing.

Follow-up 3

What are the potential issues with using Handlers?

There are a few potential issues to consider when using Handlers in Android:

  1. Memory leaks: If a Handler is not properly removed or if it holds a reference to a long-lived object, it can cause a memory leak. To avoid this, it is important to remove callbacks and messages from the Handler when they are no longer needed.

  2. Race conditions: When multiple threads are accessing a Handler, there is a potential for race conditions. It is important to properly synchronize access to the Handler to avoid unexpected behavior.

  3. Performance impact: Using Handlers can introduce some overhead, especially when posting messages or runnable objects frequently. It is important to consider the performance implications and optimize the usage of Handlers when necessary.

4. What is the JobScheduler in Android?

JobScheduler is a system service introduced in API 21 that lets you schedule deferrable background work with constraints. Instead of running a task immediately, you describe the conditions under which the job should run and hand it to the system, which batches and executes jobs efficiently to reduce battery drain.

Core concepts

  • JobInfo: Describes the job — which component runs it, what constraints must be met, and scheduling parameters.
  • JobService: A Service subclass you implement. The system calls onStartJob() on the main thread; heavy work must be dispatched to a background thread. Return true if work is ongoing asynchronously, and call jobFinished() when done.
  • Constraints you can set: network type (NETWORK_TYPE_ANY, NETWORK_TYPE_UNMETERED), battery not low, device charging, device idle, content URI triggers.

Practical limitation: use WorkManager instead

In practice, you should use WorkManager rather than JobScheduler directly. WorkManager is the Jetpack-recommended solution for deferrable background work and internally delegates to JobScheduler on API 23+. WorkManager adds:

  • Guaranteed execution (work persists across process death and reboots)
  • Chaining and parallel work graphs
  • Observable WorkInfo via LiveData or Flow
  • Kotlin coroutines support via CoroutineWorker
  • Testing support with TestDriver
val request = OneTimeWorkRequestBuilder()
    .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
    .build()
WorkManager.getInstance(context).enqueue(request)

When to mention JobScheduler directly

Know JobScheduler exists and understand it conceptually for interviews, but signal that WorkManager is the right abstraction for application code. JobScheduler is relevant if you're writing system-level or privileged apps that can't depend on Jetpack.

Common follow-up questions

  • What's the difference between JobScheduler and AlarmManager? AlarmManager fires at an exact time and wakes the device; JobScheduler is deferrable and batched by the system. For exact timing use AlarmManager; for deferrable work use JobScheduler/WorkManager.
  • What happens if a job is killed before it finishes? The system calls onStopJob(). Return true from onStopJob() to ask the system to reschedule the job.
  • How does Doze mode affect JobScheduler? Jobs are deferred during Doze mode unless the app is whitelisted. WorkManager handles this gracefully.
↑ Back to top

Follow-up 1

What are some common use cases for JobScheduler?

Some common use cases for JobScheduler include:

  1. Periodic data synchronization: You can use JobScheduler to schedule periodic tasks to synchronize data with a server.

  2. Background data processing: JobScheduler can be used to perform background data processing tasks, such as image compression or file encryption.

  3. Network operations: JobScheduler can be used to perform network operations, such as downloading or uploading files, in the background.

  4. Battery optimization: JobScheduler can be used to schedule tasks to run when the device is idle or charging, in order to optimize battery usage.

Follow-up 2

How does JobScheduler handle tasks when the device is in doze mode?

When the device is in doze mode, JobScheduler can still execute tasks, but with some restrictions. In doze mode, the device's CPU and network activity are restricted to save battery power.

JobScheduler handles tasks in doze mode by batching them together and executing them during maintenance windows. These maintenance windows are scheduled by the system and occur periodically, allowing the device to perform necessary tasks while still conserving power.

It's important to note that JobScheduler may not be able to execute tasks immediately when the device is in doze mode, as the system prioritizes power saving over task execution.

Follow-up 3

What are the differences between JobScheduler and AlarmManager?

JobScheduler and AlarmManager are both classes in the Android framework that allow you to schedule tasks or jobs to be executed at a later time. However, there are some differences between them:

  1. Flexibility: JobScheduler provides more flexibility in scheduling tasks compared to AlarmManager. JobScheduler allows you to specify conditions for task execution, such as network availability or device charging status.

  2. Power optimization: JobScheduler is designed to optimize power usage by batching tasks and executing them during maintenance windows. AlarmManager, on the other hand, may wake up the device from sleep mode to execute tasks immediately.

  3. API level: JobScheduler was introduced in Android 5.0 (API level 21), while AlarmManager has been available since the early versions of Android.

It's recommended to use JobScheduler for most background task scheduling needs, unless you specifically require the functionality provided by AlarmManager.

5. How can we use Threads in Android?

Android gives you several mechanisms for running code on background threads. The right choice depends on what you need from the threading model.

Kotlin coroutines (recommended for most cases)

Coroutines are the idiomatic, recommended approach for all new Android code. They provide structured concurrency — background work is tied to a scope and cancelled automatically when the scope ends:

viewModelScope.launch(Dispatchers.IO) {
    val data = repository.fetchData()
    withContext(Dispatchers.Main) {
        _uiState.value = data
    }
}

Dispatchers.IO is optimized for blocking I/O (network, disk). Dispatchers.Default is for CPU-intensive computation. Dispatchers.Main for UI updates.

Raw Thread and Runnable

You can still use Thread directly for simple, fire-and-forget work where lifecycle management is not a concern:

Thread {
    val result = doHeavyWork()
    runOnUiThread { updateUI(result) }
}.start()

This is error-prone — no lifecycle awareness, no easy cancellation, and runOnUiThread ties you to Activity. Use only in isolated utility code.

ExecutorService and thread pools

For parallel work where you need a fixed pool of threads, Executors.newFixedThreadPool(n) or Executors.newCachedThreadPool() are appropriate. You can integrate an Executor with coroutines using executor.asCoroutineDispatcher(). This is common in SDK or library code.

HandlerThread

A HandlerThread is a Thread with a built-in Looper, enabling a serial message queue. Useful for ordered, serial background work such as serial port communication or Bluetooth command queues.

What not to use

  • AsyncTask: Deprecated as of API 30, do not use in new code.
  • IntentService: Deprecated as of API 30; replace with WorkManager or a foreground service with coroutines.

Interview framing

Interviewers asking this question want to confirm you know the modern stack. Lead with coroutines, explain the dispatcher model, and mention raw threads only as lower-level alternatives. Know why AsyncTask was removed and what replaced it.

↑ Back to top

Follow-up 1

What are some common use cases for Threads?

Threads are commonly used in Android for performing tasks that may take a long time to complete, such as:

  1. Network operations: Threads can be used to perform network operations, such as making HTTP requests or downloading files, without blocking the main UI thread.

  2. Database operations: Threads can be used to perform database operations, such as querying or updating data, without blocking the main UI thread.

  3. Image processing: Threads can be used to perform image processing tasks, such as resizing or applying filters to images, without blocking the main UI thread.

  4. Background tasks: Threads can be used to perform any other time-consuming tasks in the background, such as parsing large amounts of data or performing complex calculations.

Follow-up 2

How can we communicate between a background thread and the main thread?

In Android, we can communicate between a background thread and the main thread using various mechanisms:

  1. Handler: A Handler is an object that allows communication between threads by sending and handling messages. We can create a Handler in the main thread and use its post() or sendMessage() methods to send messages from the background thread to the main thread.

  2. runOnUiThread() method: This method is available in the Activity class and allows us to run code on the main thread from a background thread. We can use this method to update the UI or perform any other operation that should be executed on the main thread.

  3. AsyncTask: AsyncTask provides methods for executing code on the background thread and updating the UI on the main thread. We can override the onPostExecute() method to update the UI with the results of the background task.

  4. LiveData: LiveData is an observable data holder class provided by the Android Architecture Components. It allows us to observe changes to data and automatically update the UI on the main thread when the data changes.

Follow-up 3

What are the potential issues with using Threads?

When using Threads in Android, there are several potential issues to be aware of:

  1. Memory leaks: If a thread is not properly managed and terminated, it can cause memory leaks, leading to increased memory usage and potential crashes.

  2. Race conditions: When multiple threads access shared data concurrently, race conditions can occur, leading to unexpected behavior or data corruption. Proper synchronization mechanisms, such as locks or synchronized blocks, should be used to prevent race conditions.

  3. ANR (Application Not Responding) errors: If the main UI thread is blocked for too long, the Android system may display an ANR error, indicating that the application is not responding. Long-running tasks should be performed on background threads to avoid ANR errors.

  4. UI updates from background threads: Updating the UI from a background thread can cause synchronization issues and lead to crashes or inconsistent UI state. UI updates should always be performed on the main thread using the appropriate mechanisms, such as Handlers or runOnUiThread().

Live mock interview

Mock interview: Android Threading Basics

Intermediate ~5 min Your own free AI key

Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.