In Android development, being able to handle tasks in the appropriate thread is an essential skill to have. In this series, I’m planning to cover all the available methods in Android for handling tasks in the background. As the first article of this series, I will explain the basics of background tasks and will explain how to handle a background task using a thread.
What is a background task?
The simplest answer is any task that needs to execute outside the main thread is qualified as a background task. But there are many variations of background tasks. it can be in the range of doing small network calls in the background to doing background tasks even though the application is closed. The following graph summarises the range of background tasks that we might need to implement during our Android application development.
First, a task that needs to execute in the background is divided into three groups.
- Immediate: A task need to execute as soon as possible and the user is waiting for results.
- Long-running: A Task may take more than a few minutes to complete the execution.
- Deferrable: A task that needs to be executed but has no urgency to execute at this moment itself.
And Immediate tasks are dived into further categories.
- Impersistent : A task that doesn’t need to survive application end or device reboot.
- Persistent: A task that should be continued to execute even when the application gets closed or the device restarts.
You may be wondering why there is no persistent long-running background task or deferrable persistent tasks on the above breakdown. It’s because Android does not recommend having background tasks fall into those categories.
Threads
Threading is a way of providing a set of instructions to the CPU to execute independently from its parent task. In Android, this can be used to execute immediate impersistent tasks. In Android, there are other components that work with threads that are important to know.
Runnable
Runnable
is just an interface you need to instantiate a thread to contain it. It contains a single run()
function where we need to place our code related to the background task. when the thread is executed the run()
function will be triggered.
Handler
A Handler
is a component that is used to send messages to other threads. In general, we use this to send one thread execution results to another thread to perform the next calculation on that sent data or to conclude the process.
Coding Example
Let’s take a look at the coding example. Let’s assume I receive data from a network call that is not in a proper format. I need to rearrange it and show it in a recycler view. To make it simple I’m going to store it in a file and get the details from that. The data rearranging part going to be performed in a thread and notify the main thread to show the details. The following function is responsible for constructing the data the way it’s suitable.
fun prepareMessages(response: String): ArrayList<MessageContent> {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(InboxResponse::class.java)
val inboxResponse = jsonAdapter.fromJson(response)
val messages = arrayListOf<MessageContent>()
inboxResponse?.apply {
messages.addAll(offers)
messages.addAll(loyalty)
messages.sortedBy { it.title }
}
return messages
}
The example response I’m using contains a JSON array with two objects called “offers” and “loyalty”. Let’s imagine that on other platforms there are separate tabs to show these messages But in Android, the UI design requirement is to show all the messages in one view. And it’s required to sort the data by the object property “title”. This task can be categorized as an immediate impersistent task. let’s see how this function can be executed in a thread.
First, we need to create a Runnable
and call this function inside that. That will make sure to execute the code block inside a separate thread when the Runnable
is attached. When the data is returned from the prepareMessages()
function we need to send those messages to the main thread to display on a list view. This communication is done by using a Handler
. Handler
has an object call message. To that, we can add data as a bundle. After adding the data to the Message you can send the data by calling sendMessage()
function on Handler.
val prepareMessageRunnable = Runnable {
val inboxMessages = prepareMessages(jsonDataString)
val bundle = Bundle()
bundle.putParcelableArrayList(INBOX_MESSAGES_BUNDLE_KEY, inboxMessages)
val message = handler.obtainMessage()
message.data = bundle
handler.sendMessage(message)
}
The Handler
can be implemented like the below. Provide Looper.getMainLooper()
to the Handler
to let it know that data must be consumed in the main thread. When the sendMessage(message)
get called inside the runnable function it will trigger handleMessage(msg: Message)
function. Inside handleMessage(
the bundle data that we set up before can be retrieved. Since I’m using Jetpack composer as the choice of UI design method I’m setting up those data into msg: Message
)mutableStateListOf<MessageContent>()
. So when that value is changed UI will show the recycler view with the merged and sorted data.
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
val inboxMessages = if (Build.VERSION.SDK_INT >= 33) {
msg.data.getParcelableArrayList(
INBOX_MESSAGES_BUNDLE_KEY,
MessageContent::class.java
)
} else {
msg.data.getParcelableArrayList(INBOX_MESSAGES_BUNDLE_KEY)
}
inboxMessages?.let {
inboxMessagesState.addAll(it)
loadingState = false
}
}
}
To make all these things work in a new thread, a thread needs to be created and attach the created runnable to it. When that thread is started above-explained steps will be executed. A new thread can be created and started like below.
LaunchedEffect(key1 = inboxMessagesState){
val prepareMessageThread = Thread(prepareMessageRunnable)
prepareMessageThread.start()
}
You can find the full code in this repository. In the main application UI, click on the “Thread” button to see the above example running.