How to Manage the Android Main Thread: Coroutines

When the user launches your app, Android creates a new Linux process along with an execution thread. This main thread, also known as the UI thread, is responsible for everything that happens on the screen. Understanding how it works can help you design your app to use the main thread for the best possible performance.“

https://developer.android.com/topic/performance/threads#main

Our applications require a time frame of 16 milliseconds to render 60 Hz in the UI smoothly and avoid Janks, or in the worst case scenario, ANRs (Application Not Responding).

As the documentation states, we have to understand how the Main Thread (UI) works in order to deliver the best possible performance. But how can we achieve the “best possible performance” when our application has a lot of heavy processes that can compromise it? Fortunately, asynchronous/background processes are here to help.

In this article, we will show you how to manage a UI thread using this example:

We want to show the detail of a Taxi App. To accomplish this, we have the following functions:

 

fun getTrip(tripId: String): Trip {}
fun getTripDriver(userId: String): Driver {}
fun getTripBill(billId: String): Bill {}

fun showTaxiTripDetail(tripId: String) {
	val trip = getTrip(tripId) -----+ Network
	val driver = getTripDriver(trip.userId) | ---> Proccesses
	val bill = getTripBill(trip.billId) ----+
	renderTripDetail(trip, driver, bill)----+ ---> UI Render
}

 

Now, we will be solving this task using Kotlin Coroutines + Retrofit + MVP. It’s important to always keep in mind that we want to keep our Main Thread free of Janks or ANRs in order to understand the concepts of Coroutine Scope and Coroutine Context. Together they will allow us to run long/heavy tasks or update our UI properly.

This is the Main View where the result data will be shown:

 

class TaxiTripDetailFragment() : Fragment(), TaxiTripDetailView {
	override fun onCreateView(..).. {}
    
    override fun renderTripDetail(trip: Trip, driver: Driver, bill: Bill) {
    	Toast.makeText(
          context.applicationContext,
          "Trip: ${trip.id}, Driver: ${driver.name}, Bill: ${bill.price}",
          Toast.LENGTH_LONG
        ).show()
	}
}

 

So, we have a presenter called TaxiTripDetailPresenter where the asynchronous process will happen:

 

class TaxiTripDetailPresenter(private val view: TaxiTripDetailView) {
	private val job = Job()
    private val scopeMainThread = CoroutineScope(job + Dispatchers.Main)
    private val scopeIO = CoroutineScope(job + Dispatchers.IO)
    
    private val repository = TaxiTripRepositoryNetwork()
    fun showTaxiTripDetail(tripId: String) {
    	scopeIO.launch {
        	val trip = repository.getTrip(tripId)
            val driver = repository.getTripDriver(trip.userId)
            val bill = repository.getTripBill(trip.billId)
            scopeMainThread.launch {
            	view.renderTripDetail(trip, driver, bill)
            }
        }
    }
    
    fun onDestroy() {
    	job.cancel()
    }
 }           

 

Then, we have the class TaxiTripRepositoryNetwork to make the requests to the endpoints:

 

class TaxiTripRepositoryNetwork {
	private val service = TaxiTripService()
    
    suspend fun getTrip(tripId: String): Trip {
    	return service.getTrip(tripId).await()
    }
    
    suspend fun getTripDriver(userId: String): Driver {
    	return service.getTripDriver(userId).await()
    }
    
    suspend fun getTripBill(billId: String): Bill {
    	return service.getTripBill(billId).await()
    }   
 }   

 

And, finally, the class TaxiTripService, which is the class with the endpoints that we want to consume using Retrofit:

 

interface TaxiTripService {

	@GET("trips/{tripId}")
    fun getTrip(@Path("tripId") tripId: String): Deferred<Trip>
    
    @GET("users/{userId}")
    fun getTripDriver(@Path("userId") userId: String): Deferred<Driver>
    
    @GET("bills/{billdId}")
    fun getTripBill(@Path("billdId") billId: String): Deferred<Bill>
}

Note: The classes and architecture defined for this example can be changed or improved.

There are a lot of new concepts to cover here. All of them will help us create async/background processes to manage the Main Thread:

 

Coroutine

Coroutines are light-weight threads.

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.

According to Donald Knuth, Melvin Conway coined the term coroutine in 1958 when he applied it to the construction of an assembly program.[1] The first published explanation of the coroutine appeared later, in 1963.

Conway, M. E. (July 1963). "Design of a Separable Transition-Diagram Compiler". Communications of the ACM. 6 (7): 396–408. doi:10.1145/366663.366704

Coroutines are not a new concept. They have been adopted by many programming languages since 1963. Nowadays, C# uses this concept and practically uses the same path to make async/background processes. Kotlin adopted this concept to make the async/background processes lightweight, making them easy to use and read.

Suspend:

Functions with the suspend modifier are regular functions that will allow the execution of a coroutine to be suspended.

The modifier is added at the beginning of the regular function sign to run other suspend functions. Those functions can only be called within other suspend functions or coroutine.

Structure: (Like a regular function)

suspend fun getTripBill(billId: String): Bill {
	return service.getTripBill(billId).await()
}

 

For this example, we are running another suspend function which is .await(). This is the signature of the await function:

 

public suspend fun await(): T

 

Await:

This function “awaits” for the completion of a computed value without blocking a thread. It resumes execution by returning the final value or throwing the corresponding exception if the deferred was canceled or if there was an error.

 

Structure:

You have to use await if you want to get a result from a Deferred transaction. So, in our example, we are using:

return service.getTripBill(billId).await()

 

This is so because the function getTripBill(billId)will return a Deferred value that contains our result; in this case, the Bill object.

 

@GET("bills/{billdId}")
fun getTripBill(@Path("billdId") billId: String): Deferred<Bill>

 

Deferred<T>

A Deferred value is a non-blocking cancellable future. It is cancellable because we can cancel the process and future since we will obtain a value from this. But how can we get a value from a Deferred? We mentioned it above: await().

It is like using a RxJava Single (Single<Bill>) or a Retrofit Call (Call<Bill>). You can cancel them and/or retrieve a value in the future.

A Deferred is a like a Job because it has a lifecycle that culminates in its completion, the difference is that a Deferred will return a value via the await() function which belongs EXCLUSIVELY to the Deferred.

 

Structure:


@GET("bills/{billdId}")
fun getTripBill(@Path("billdId") billId: String): Deferred<Bill>

In this case, the function getTripBill(..)will return a Bill as a result when the service returns a response.

Job

As we saw in the Deferred section, conceptually, a job is a cancellable element with a life-cycle that culminates in its completion. Unlike a Deferred, a job does not produce a result value.

Structure:

In the TaxiTripDetailPresenter, we declared a Job that enables us to cancel all the children attached to it in order to avoid memory leaks from the Running Coroutines.

private val job = Job()
private val scopeMainThread = CoroutineScope(job + Dispatchers.Main)
private val scopeIO = CoroutineScope(job + Dispatchers.IO)

The Job + Dispatchers.Main or Dispatchers.IO are part of a CoroutineContext that will be enclosing the CoroutineScope in the implementation of the coroutine builders (async, launch), i.e. where the Coroutine will be executed.

 

CoroutineScope

The coroutines belong to a local scope, which will hopefully terminate instead of running indefinitely.

Structure:

In the TaxiTripDetailPresenter, we are declaring two Scopes:

One will run on the Android Main Thread,

private val scopeMainThread = CoroutineScope(job + Dispatchers.Main)

And the other will run I/O operations in a secondary thread,

private val scopeIO = CoroutineScope(job + Dispatchers.IO)

As stated, the Coroutine Scope should terminate. We must consider the lifecycle of our application, as we are doing here, in the TaxiTripDetailPresenter:

fun onDestroy() {
	job.cancel()
}

The job will cancel all its associated children and release the memory used to run the Coroutines. If the job is not canceled, it can lead to memory leaks because it will continue listening for coroutine updates.

If you want to launch a scope that is not attached to a lifecycle, you can use GlobalScope. But as the documentation states, this is not recommended:

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not canceled prematurely.

Application code usually should use an application-defined CoroutineScope (which we go through in our example). Using async or launch on the instance of GlobalScope is highly discouraged.

So, having a Coroutine running throughout the whole application lifetime can be prone to memory leaks.

 

Dispatchers or CoroutineDispatcher:

The CoroutineContext, as we previously saw, is composed of a Job + Dispatcher:

private val scopeIO = CoroutineScope(job + Dispatchers.IO)

With the Dispatcher, we are telling the Coroutine in which thread it will be running.

The Coroutine Builders (async, launch) receives an optional parameter, a CoroutineContext.

The Dispatchers we have with Coroutines are:

  1. Dispatchers.Default: Executed in a shared pool of threads. By default, this dispatcher will create a thread for every available CPU core.
  2. Dispatchers.IO: Designed for blocking I/O tasks, such as HTTP requests, imaging processing, reading/writing disk, reading/writing in a local database. By default, this dispatcher is limited to 64 CPU cores.
  3. Dispatchers.Main: Allows coroutines to run in the UI Main Thread. Needs kotlinx-coroutines-android dependency.
  4. Dispatchers.Unconfined: This dispatcher is not confined to any thread. It will be launched in the caller thread and will be resumed in the suspended function that was invoked. This is appropriate for Coroutines that don’t consume a lot of CPU or don’t require UI updates.

So, for these lines in the presenter TaxiTripDetailPresenter:

private val scopeMainThread = CoroutineScope(job + Dispatchers.Main)

This will be used to run the result in the Android Main Thread (UI) after obtaining the result from the services.

 

private val scopeIO = CoroutineScope(job + Dispatchers.IO)

This will be executed in a shared pool of threads for an IO task; in this case, a network request.

 

Coroutine Builders:

Async: when we want to run something, then await the result.

Launch: when we want to run and forget; i.e. a result is not returned.

In the presenter, we have:

scopeIO.launch { 		                                    --------------+
	val trip = repository.getTrip(tripId) 						  	      |
	val driver = repository.getTripDriver(trip.userId                     |
	val bill = repository.getTripBill(trip.billId) 				  		  |--IO Scope
	scopeMainThread.launch {		 			------+                   | 
    	view.renderTripDetail(trip, driver, bill) 	  | - UI Scope        |
	} 											------+                   |
} 														    --------------+

In our example, we are not using the Coroutine Builder async because we don’t need a result when we launch our Coroutine but it will be exactly the same as launch.

If we remove the scopeMainThread.launch (UI Scope) we will get an exception:

java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()

This is because we are trying to update/show something in the UI from a thread (IO Scope) other than the UI thread.

Finally, if you want to use android Coroutines in your project, use these dependencies:

{last_version}: https://github.com/Kotlin/kotlinx.coroutines
dependencies {
	implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:{last_version}'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:{last_version}'
}

 

References:

https://en.wikipedia.org/wiki/Coroutine

https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/

https://github.com/Kotlin/kotlinx.coroutines/tree/master/docs

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/index.html

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/

 

Contact Us

Share this post

Table of Contents