Kahibaro
Discord Login Register

20.1 Threads and Handlers

Understanding Background Work on Android

In Android, the user interface runs on a single thread called the main thread or UI thread. All drawing, view updates, and user input processing are handled there. To keep apps responsive, long or expensive work must be moved off this thread. Threads and handlers are the most basic tools Android provides for this.

This chapter focuses on how to create and manage threads, and how to communicate safely with the UI thread using handlers. You will see how these pieces fit together in practice, and what typical patterns look like in simple apps.

The Main Thread and Why Extra Threads Are Needed

Every Android app process starts with one main thread. All Activity, Fragment, and view lifecycle methods run on this thread. If you do heavy work here, such as network calls or large file processing, the UI blocks and the system may show an "Application Not Responding" dialog.

To avoid this, you create additional threads. A thread is a separate path of execution that can run code in parallel with the main thread. That other thread can perform the heavy work while the main thread keeps the app interactive. When the work finishes, the result must be handed back to the main thread before you update any UI elements, because only the main thread may modify views.

Important rule: Never perform long running work or blocking operations on the main thread. Never update Android UI elements directly from background threads.

Threads give you a low level mechanism for background work, while handlers help you coordinate messages and runnable tasks between threads.

Creating and Starting Threads

In Kotlin, there are several ways to create a new thread. At the core is the Thread class from the Java standard library, which Android uses.

A simple way is to pass a Runnable to a Thread and call start:

val workerThread = Thread {
    // This code runs on the worker thread
    Thread.sleep(2000) // simulate long work
    println("Background work finished")
}
workerThread.start()

The lambda passed to Thread is the background work. start() creates the thread and begins executing this code. If you call run() instead of start(), the code runs on the current thread, not a new one, so always use start() to get a new thread.

You can also subclass Thread and override run:

class MyWorkerThread : Thread() {
    override fun run() {
        // This runs on the background thread
        // Example: heavy computation
    }
}
val t = MyWorkerThread()
t.start()

Subclasses are more verbose and are less common in simple Android code. Most of the time, you use the lambda style or thread pools, which are beyond this chapter.

Threads can be named to make debugging easier:

val worker = Thread(
    {
        // background work
    },
    "ImageLoaderThread"
)
worker.start()

If a thread is created as a daemon thread in Java, it can be stopped automatically when the process exits. On Android, you typically let the system control process lifetimes instead of relying on daemon threads.

Joining and Interrupting Threads

Once a thread starts, it runs until its run block finishes. Sometimes you need to wait for a thread to complete, or request that it stop early.

The join method blocks the current thread until the other thread finishes:

val worker = Thread {
    Thread.sleep(1000)
    println("Worker done")
}
worker.start()
// This blocks the caller thread until worker finishes
worker.join()
println("Now safe to continue after worker")

You rarely call join from the main thread, because that would freeze the UI. It can be useful in tests or strictly background coordination.

To request early termination, you can interrupt a thread:

val worker = Thread {
    try {
        while (!isInterrupted) {
            // Do periodic work
            Thread.sleep(500)
        }
    } catch (e: InterruptedException) {
        // Thread was interrupted while sleeping
    }
}
worker.start()
// Later, request it to stop
worker.interrupt()

Interruption is cooperative. The thread checks isInterrupted or handles InterruptedException and then exits its loop by returning from run. You must design your thread code to respond to interruptions.

On Android, interruption is useful when you want ongoing background work to stop when the user leaves a screen or cancels an operation.

Why UI Access from Threads Is Restricted

Android view classes like TextView and Button are not thread safe. Their internal state is built around the expectation that only the main thread will touch them. If multiple threads updated views, you might see race conditions, corrupted state, or crashes.

Because of this, calling something like textView.text = "Done" from a background thread is unsafe. In practice, you may see exceptions like:

android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views.

To update the UI after background work, you must post back to the main thread. Handlers provide one way to do this. They can schedule code to run on a specific thread, usually the main thread, without breaking this rule.

Message Queues, Loopers, and Handlers

Each thread can optionally have a message queue and a Looper. The looper stays alive and continuously takes messages from the queue, then dispatches them to handlers associated with that looper. The main thread in Android has a looper and a message queue already configured by the framework.

A handler is tied to a looper. It can put messages and runnable tasks into the looper's queue. When the looper processes these items, it calls the handler to run the code on the looper's thread. In regular app code, you usually interact only with the handler and do not manage the looper directly.

On the main thread, this means you can post tasks from any background thread and be sure they run safely on the main thread.

Creating a Handler on the Main Thread

The main thread already has a looper. To post work to it you create a Handler that uses Looper.getMainLooper().

import android.os.Handler
import android.os.Looper
// Create once, for example in an Activity
private val mainHandler = Handler(Looper.getMainLooper())

The handler now has a reference to the main looper. You can post tasks that will be executed on the main thread.

The most common method is post, which takes a Runnable (in Kotlin a lambda):

mainHandler.post {
    // This code runs on the main thread
    textView.text = "Updated from background"
}

If you call this from a background thread, the lambda is put into the main thread's message queue. The looper later runs it on the main thread. This is the standard pattern to update UI after doing background work.

You can also schedule work with a delay using postDelayed:

// Execute after 2000 milliseconds on the main thread
mainHandler.postDelayed({
    textView.text = "Updated after 2 seconds"
}, 2000)

Delayed tasks are useful for loading indicators, small timeouts, and debounced UI actions.

Sending Messages with Handlers

Handlers can also send typed Message objects instead of generic runnables. This is more verbose but can be useful when you want structured communication between threads.

A Message can carry an integer what code, two integer arguments arg1 and arg2, and an obj reference for arbitrary data.

To use this style you subclass Handler and override handleMessage:

class MyHandler(looper: Looper) : Handler(looper) {
    override fun handleMessage(msg: Message) {
        when (msg.what) {
            1 -> {
                val value = msg.arg1
                // Handle type 1 message
            }
            2 -> {
                val data = msg.obj as? String
                // Handle type 2 message
            }
        }
    }
}
// Create on the main thread
val myHandler = MyHandler(Looper.getMainLooper())

From a background thread you can send a message:

val msg = myHandler.obtainMessage()
msg.what = 1
msg.arg1 = 42
myHandler.sendMessage(msg)

Or use the shortcut:

myHandler.sendMessage(
    myHandler.obtainMessage(2, "Hello from worker")
)

For most beginner Android code, runnable based post calls are simpler and easier to understand than manually creating messages.

Building a Simple Thread plus Handler Pattern

A common pattern is to run heavy work on a background thread, then update the UI with the result on the main thread using a handler.

Consider loading a large list from disk without blocking the UI:

class MainActivity : AppCompatActivity() {
    private val mainHandler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val statusText = findViewById<TextView>(R.id.statusText)
        Thread {
            // This block runs on a worker thread
            val items = loadItemsFromDisk() // long operation
            // Now post the update back to the main thread
            mainHandler.post {
                statusText.text = "Loaded ${items.size} items"
            }
        }.start()
    }
    private fun loadItemsFromDisk(): List<String> {
        Thread.sleep(2000) // simulate slow I/O
        return listOf("One", "Two", "Three")
    }
}

The key steps are clear. First, the thread does the long work. Second, instead of touching statusText directly from the worker thread, it posts a runnable to the main handler. The runnable runs on the UI thread and updates the view.

This pattern appears frequently when you do network requests or database queries in basic Android apps.

Creating Background Threads with Loopers

The main thread already has a looper. If you want a background thread that can also receive messages and runnables through a message queue, you can create one using HandlerThread.

HandlerThread is a convenient Android class that starts a new thread and prepares a looper for it. You then create a handler using that looper.

import android.os.HandlerThread
// Create and start a handler thread
val handlerThread = HandlerThread("WorkerThread")
handlerThread.start()
// Create a handler attached to this background thread
val workerHandler = Handler(handlerThread.looper)
// Post work to the background thread
workerHandler.post {
    // This runs on the handlerThread
}

Later, when you are done with the thread, you should stop its looper:

handlerThread.quitSafely()

HandlerThread is useful when you want a long lived background thread that processes tasks sequentially, without starting a new Thread for every single job.

Cleaning Up Handlers and Avoiding Leaks

Because handlers can run delayed tasks, they might hold references to activities or views longer than you expect. If you post a delayed runnable that captures a reference to an Activity, and the activity is destroyed before the runnable executes, the runnable can keep the activity in memory and cause a memory leak.

To avoid this, you should clear pending callbacks when a component is destroyed, and avoid capturing strong references when not needed.

In an activity, you can remove callbacks in onDestroy:

class MainActivity : AppCompatActivity() {
    private val mainHandler = Handler(Looper.getMainLooper())
    private val updateRunnable = Runnable {
        // Update UI
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mainHandler.postDelayed(updateRunnable, 5000)
    }
    override fun onDestroy() {
        super.onDestroy()
        // Remove callbacks to avoid leaks
        mainHandler.removeCallbacks(updateRunnable)
    }
}

removeCallbacks ensures the runnable will not run after the activity is gone.

A more advanced pattern uses a weak reference inside the runnable or handler, so the activity can be collected. For beginners, explicitly removing callbacks in lifecycle methods is a clear and practical technique.

Thread Safety and Shared Data

When more than one thread reads and writes a shared variable, you may get race conditions. For example, you might increment a counter from several threads and end up with an incorrect final value, because the operations interleave in unexpected ways.

Simple example:

var count = 0
val t1 = Thread {
    repeat(1000) { count++ }
}
val t2 = Thread {
    repeat(1000) { count++ }
}
t1.start()
t2.start()
t1.join()
t2.join()
println(count) // Might not be 2000

The result can be less than 2000 because count++ is not atomic. Proper synchronization involves tools like synchronized, volatile, AtomicInteger, and others, which are outside the focus of this chapter. The important point is that you should be careful when multiple threads share mutable data.

Handlers can help by funneling all modifications to certain state through a single thread. If all updates to a variable happen on the same handler and thread, you avoid concurrent writes.

When to Prefer Threads and Handlers

Threads and handlers are the most basic and low level form of background work available in Android. They are useful for simple tasks, educational purposes, and cases where you need very direct control.

However, they scale poorly for complex apps. As your app grows, you often switch to higher level tools that build on top of threads, such as Kotlin coroutines, or WorkManager for deferrable background tasks. Those tools simplify cancellation, error handling, and lifecycle awareness.

You still benefit from knowing how threads and handlers work, because many Android components and libraries are built on top of these primitives. Understanding the basic rules, such as "UI only on the main thread" and "use handlers to communicate between threads," will help you reason about more advanced tools later.

Summary of Key Practices

Threads let you move heavy work off the main thread. Handlers let you send tasks back to a particular thread, usually the main one, so you can safely update the UI.

The core practical pattern is simple. First, start a background thread to perform a long operation. Second, when the operation finishes, post a runnable to a handler on the main thread that reads the result and updates the views. Always respect lifecycle events by removing pending callbacks and, when needed, cancelling work when a screen is no longer visible.

By applying these ideas your apps remain responsive while they perform real work in the background.

Views: 1

Comments

Please login to add a comment.

Don't have an account? Register now!