Coroutines: simplify threading on android

Android runs on Java and by default Java threads execute once, and once only. On a platform like Android where the UI is constantly updating, creating new threads for every action quickly becomes expensive. To get around this Android’s main thread uses the HandlerThread which is a subclass of the Java Thread. Android keeps this main thread alive by using a Looper which schedules Messages (Runnables) to execute.

These messages are run sequentially, which is why performing long running messages on the main thread will block subsequent messages waiting to be executed. This is why network calls, database calls, and long running operations, must all be run on an alternative thread — to avoid blocking the main thread (the UI).

Threading is complicated

As can be seen by the adoption, and abandonment of a variety of popular threading methods: AsyncTask, ThreadExecuters, EventBus, RxJava (to be abandoned? With the release of Flow in coroutines 1.3, this looks likely!). So are coroutines any different? And can coroutines simplify threading architecture on Android?

Coroutines

First add these lines to your build.gradle file.

// Ensure you are using Kotlin 1.3
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2'

And you are ready to go.

fun simpleThreadingExample() {
GlobalScope.launch {
myApi.makeNetworkCall()
}
}

This is the most basic form asynchronous calls can take with coroutines. The GlobalScope defines a common pool of shared threads which all scoped coroutines will run on unless defined otherwise. By invoking the launch coroutine from GlobalScope we scope the coroutine to the lifecycle of the application and run all work on this common pool of shared threads.

However, in Android we usually need to cancel work if the lifecycle of a screen ends and asynchronous work usually results in additional work being done on the main thread. Neither of these is simple to achieve with GlobalScope. So lets try a better approach.

Localising scope

In an Android app the majority of blocking calls are scoped to specific Activities, Fragments, or Views rather than the Application. So lets create our own CoroutineScope, and in addition, we will define which thread these scopes will run on.

// This will usually be defined in your ViewModel
protected val myScope = CoroutineScope(Dispatchers.IO)
fun simpleThreadingExample() {
myScope.launch {
myApi.makeNetworkCall()
}
}

Dispatchers allow us to define the thread a coroutine runs on. Dispatchers.IO defines a shared pool of threads and all coroutines belonging to myScope will run on these shared threads.

By defining our own CoroutineScope we now have better control of the coroutines we launch, which will later allow us to cancel coroutines when certain lifecycle events occur.

Displaying a result

Usually we want to return a result from blocking calls. This can then be displayed to the user. But to display the result, we need access to the view— on the main thread.

// This will usually be defined in your ViewModel
protected val myScope = CoroutineScope(Dispatchers.Main)
fun updateView() {
myScope.launch {
val user = withContext(Dispatcher.IO) {
myApi.getUser()
}
view?.showUser(user)
}
}

To achieve this we first use the Dispatcher.Main for our CoroutineScope. This ensures all scoped coroutines run on the main thread. However, we still need a way to switching to a background thread within a Coroutine.

This is where withContext comes in. We use the withContext coroutine with the Dispatcher.IO to execute the blocking call off of the main thread. The parent coroutine (launch) suspends while the withContext coroutine is in progress, allowing the main thread to continue unimpeded. When withContext returns a result, the launch coroutine becomes active and the view is updated on the main thread.

Providing dispatchers

While we’re here, lets ensure we only define each Dispatcher once by creating a provider class and passing this class to wherever a coroutine is launched from.

class DispatcherProviderImpl : DispatcherProvider {

override val main: CoroutineDispatcher
get() = Dispatchers.Main

override val background: CoroutineDispatcher
get() = Dispatchers.IO
}
class BaseViewModel(
dispatcherProvider: DispatcherProvider
) {
protected val myScope = CoroutineScope(dispatcherProvider.main)}class MainViewModel(
private val view: View,
private val myApi: MyApi,
dispatcherProvider: DispatcherProvider,
) : BasePresenter(dispatcherProvider) {
fun updateView() {
myScope.launch {
val user = withContext(dispatcherProvider.background) {
myApi.getUser()
}
view.showUser(user)
}
}

Now our Dispatchers are defined in only one place. If we want to change the thread all our background calls run on, we simply change the Dispatcher here. The additional benefit comes when we test our code. When we initiate our class under test, instead of passing a mock to the constructor we can pass a TestDispatcherProvider. The TestDispatcherProvider always returns Dispatchers.Unconfined which ensures the coroutines launched in tests run synchronously.

/**
* Defined in your test package
*/
class TestDispatcherProvider : DispatcherProvider {

override val main: CoroutineDispatcher
get() = Dispatchers.Unconfined

override val background: CoroutineDispatcher
get() = Dispatchers.Unconfined

}

Now all tests will run synchronously on the main thread. Allowing unit tests like this.

class MainViewModelTest {

private val dispatcherProvider = TestDispatcherProvider()
private val view: View = mockk()
private val myApi: MyApi = mockk()
private val presenter = MainPresenter(
view,
myApi,
dispatcherProvider
)

@Test
fun `User is displayed`() {
val user = aUser()
every { myApi.getUser() } returns user
presenter.performBlockingTaskAndUpdateView()

verify { myApi.doBlockingTask() }
verify { view.showUser(user) }
}
}

Our individual unit tests aren’t even aware that asynchronous code exists. We’re testing the logic of the unit. Not the thread the logic occurs on.

Error handling

You may notice that if an error were to occur, we don’t handle it. The app would simply crash. So we need a way to catch errors, and take an action on the view to inform the user.

fun handleError() {
myScope.launch {
try {
val user = withContext(dispatcherProvider.background) {
myApi.getUser()
}
view?.showUser(user)
} catch (exception: Exception) {
// Handle custom errors here
view?.showError()
}
}
}

Here, we can wrap our blocking call in a try-catch. If an error occurs while the call is in progress, we catch the error, check the exception, and display an error to the user.

Cancelling coroutines

In Android we also have to worry about asynchronous calls returning after the user backgrounds the app. You don’t want to cause a crash by updating the view while the app is backgrounded. So lets make our coroutines cancelable onStop.

abstract class BaseViewModel {

protected var coreroutineSupervisor = SupervisorJob()
protected var myScope = CoroutineScope(
dispatcherProvider.main + coreroutineSupervisor
)
override fun onStart() {
if (coreroutineSupervisor.isCancelled) {
coreroutineSupervisor = SupervisorJob()
coroutineScope = CoroutineScope(dispatcherProvider.main+ coreroutineSupervisor)
}
}
// Call this from the lifecycle method of your android component
@CallSuper
override fun onStop() {
coreroutineSupervisor.cancel()
}
}

Every CoroutineScope has a Job. If you don’t initialise the scope with a Job, it is created by default. Every created coroutine also returns a Job, which is added as a child of the CoroutineScope Job of the outer coroutine. This ensures that all inner coroutines can be cancelled when the outer scope is ended.

The default behaviour of a Job is that if any child fails, all Jobs end — every child is linked to every other child. This is where the SupervisorJob comes in, the supervisor enables children to fail independent of other children.

And when our Android component reaches a paused state, we simply cancel the supervisor job. Which cancels all children jobs. So a null view is never called.

You may have noticed that there has been no mention of suspend, Deferred, async {} and await(). The reason is that combining launch and withContext ensures all long running actions occur on another thread. We can call a function, a database, or the network, and be safe in the knowledge that all these actions are non-blocking.

Why is this important? Because threading is complicated, threading makes tests more complex, and the way we thread, like a library, can change — think of AsyncTasks, ThreadExecuters, EventBus, and RxJava. By restricting threading to a single layer (the presenter layer), we can write UseCases, Repositories, Networking, Caching, and Databases as simple synchronous code.

There you have it, only one layer in your app knows about threading. The rest of your app is standard synchronous code — easy to test and easy to understand.

As ever architecture is always evolving, and there is no single ‘right’ approach. I’d love to hear any feedback you may have on this approach. Additionally, as coroutines are so new, I’m sure there’s something that has been missed so if you spot any problems or improvements please drop a comment!

Android Developer at Octopus Energy

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store