16 minutes read

In this topic, you will learn about the Handler class from the Android SDK, which is used for scheduling arbitrary tasks on a particular thread. A Handler operates with a queue of messages. It allows us to schedule code execution at a certain point in the future, with or without a specified delay. Handlers also enable us to execute code outside of our thread.

Don't worry if these concepts aren't clear enough yet — looking at some examples will make them easier to understand.

Runnable execution

To see how Handlers can be used, we will create an operation to be performed with a certain delay. Specifically, we will create a function that changes the color of an activity.

Let's start by defining the array of colors that we want to loop through:

private val colors = arrayOf(Color.RED, Color.GREEN, Color.BLUE)
private var color = colors[0]

Next, we need a Handler for our event loop inside the main thread:

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

When importing the Handler class, the IDE will offer two choices: android.os.Handler and java.util.logging.Handler. Be sure to choose the Android-related one!

Now let's declare a Runnable object with our logic in a function called run:

private val updateLight: Runnable = object : Runnable {
    override fun run() {
        color = colors[(colors.indexOf(color) + 1) % colors.size]
        window.decorView.setBackgroundColor(color)
        handler.postDelayed(this, 1000)
    }
}

As you might have already guessed, window.decorView.setBackgroundColor() is a (non-production) way to change the color of the whole activity without addressing specific elements. When we use the postDelayed method, we pass in two arguments: this (our Runnable object itself) and the delay time (in milliseconds) before the Runnable must be executed. In onStart, we simply run the following:

override fun onStart() {
    super.onStart()
    handler.postDelayed(updateLight, 1000)
}

This gives us an activity that flashes our three colors with a one-second interval! We could also have utilized post(Runnable r) to execute the Runnable without any delay.

If we had tried to use Thread.sleep to create this effect, our application would have frozen forever, making it impossible to draw the next frame or receive any input. Using a Handler allowed us to avoid this situation.

We already know that we can't load the main thread with long tasks. For example, we shouldn't create IO tasks like accessing databases, accessing files, or interacting with networks in the main thread.

From the official documentation:

Executing IO operations on the main thread is a common cause of slow operations on the main thread, which can cause ANRs. It’s recommended to move all IO operations to a worker thread.

ANR is a type of error, and the abbreviation stands for "Application Not Responding."

We must also remember to stop the execution of our task with the removeCallbacks() method if the activity is stopped:

override fun onStop() {
    super.onStop()
    handler.removeCallbacks(updateLight)
}

This ensures that our activity will stop flashing if it isn't visible and, if it gets destroyed, we will avoid a memory leak and prevent any interaction with detached views.

Multithreading with Handlers

Now let's try to create a separate thread so that we can use it to increment a counter and display the result in counterTextView:

button.setOnClickListener {
    thread {
        for (i in 0..5) {
            counterTextView.text = i.toString()
            Thread.sleep(100) // let's pretend we're doing some work
        }
    }
}

If you attempt to run this code, you're sure to catch an error. This is because we can only touch our UI elements from the main thread. To solve this problem, we again need to use a Handler.

So, let's utilize a handler object and post() our logic to it:

button.setOnClickListener {
    thread {
        for (i in 0..5) {
            handler.post {
                counterTextView.text = i.toString()
            }
            Thread.sleep(100) // let's pretend we're doing some work
        }
    }
}

As you can see, we are looping through the values in a separate thread and using handler.post() to apply the text setting.

You can view another small example below. We start by creating a StringBuilder variable to store the characters of the alphabet:

val sb = StringBuilder()
button.setOnClickListener {
    button.isEnabled = false
    sb.setLength(0)
    thread {
        for (char in 'a'..'z') {
            val string = sb.append(char).toString()
            handler.post {
                counterTextView.text = string
                if (char == 'z')
                    button.isEnabled = true
            }
            Thread.sleep(100) // let's pretend we're doing some work
        }
    }
}

Every 100 milliseconds, we increment the line containing the alphabet and append another character. This has been achieved by creating a special thread with a delay that results in the string's progressive expansion. handler.post() is required so that we can access the main thread and touch views.

Additional features

In addition to those used in the above examples, the following functions are also available:

  • postAtTime(@NonNull Runnable r, long uptimeMillis) — used to specify a time when the action should be performed. It will run when SystemClock.uptimeMillis() >= uptimeMillis.
  • postAtFrontOfQueue (Runnable r) — causes the Runnable r to be executed on the next iteration through the message queue.

Conclusion

You now know that Handlers work with message queues and have discovered how to use them to interact between threads and perform delayed operations. It's time to consolidate what you've learned!

36 learners liked this piece of theory. 10 didn't like it. What about you?
Report a typo