Coroutines, Kotlin and Android: a practical guide

Mauricio Andrada
10 min readAug 1, 2023

Objective

In this article I intend to provide a practical guide on the use of coroutines. There are a good number of books and documents online that explain its core concepts extensively, including the inner workings of the libraries and compiler, so I’ll not cover them here but will put references at the end.

How to use builders, scopes, contexts, jobs, suspension points, continuations and dispatchers will hopefully be clear by the end of this article.

The article will focus on Android, and the code will use an Activity, since it naturally uses the main thread.

Base code

Below is the base code that will be used in this article. The code will be modified for different use cases and the outputs after each modification will be used to show the differences from each use.

The code will start using the suspending function delay() to emulate long running executions. A suspending function is identified by the keyword suspend, and the places where a suspending function is called are suspending points — points in the code where the execution is suspended.

package com.onepeloton.testcoroutine

import android.os.Bundle
import androidx.activity.ComponentActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {

private val coroutineScope = CoroutineScope(Dispatchers.Main)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("onCreate 1 ${Thread.currentThread()}")
playWithCoroutines()
println("onCreate 2 ${Thread.currentThread()}")
}

private fun playWithCoroutines() {
println("playWithCoroutines 1 ${Thread.currentThread()}")
coroutineScope.launch {
println("playWithCoroutines 2 ${Thread.currentThread()}")
execDelay(10, 1)
execDelay(12, 2)
execDelay(15, 3)
println("playWithCoroutines 3 ${this.coroutineContext}")
}
println("Finished playWithCoroutines 4 ${Thread.currentThread()}")
}

private suspend fun execDelay(delay: Long, index: Int) {
println("execDelay $index ${Thread.currentThread()}")
println("start $delay")
delay(delay*1000)
println("end $delay")
}

override fun onPause() {
super.onPause()
println("onPause 1 ${Thread.currentThread()}")
}

override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
println("onDestroy 1 ${Thread.currentThread()}")
}

override fun onResume() {
super.onResume()
println("onResume 1 ${Thread.currentThread()}")
}
}

Code analysis

Line 14: private val coroutineScope = CoroutineScope(Dispatchers.Main)

This one deceitfully simple line of code hides a number of core concepts when using coroutines.

It creates an instance of CoroutineScope. The scope is needed for establishing the coroutine context for the execution.

A coroutine context is normally the combination of a Job, responsible for managing the lifecycle of the coroutine execution, and a Dispatcher, responsible for the thread used for the execution.

Here only the dispatcher was provided — in the example the Main dispatcher — so the code will construct a job for this coroutine.

Line 21: coroutineScope.launch {

The launch function is an extension function of CoroutineScope and is called a coroutine builder.

Internally, it creates an instance of class StandaloneCoroutine and starts it, using the context provided by the scope — in this case the Main dispatcher. It then returns the coroutine instance as an instance of a Job, so the code can control its execution.

The lambda block is then scheduled for execution as a suspending function, but the actual execution will depend on the selected thread.

Line 48: coroutineScope.cancel()

This line cancels the scope and any coroutines scheduled for execution. It’s called in the onDestroy() function of the Activity to indicate the scope is bound to the Activity and, once the Activity is gone, so must be the coroutines.

Lines 26, 27 and 28: calls to execDelay()

Again several things happening here. Function execDelay() is defined as:

    private suspend fun execDelay(delay: Long, index: Int) {
println("execDelay $index ${Thread.currentThread()}")
println("start $delay")
delay(delay*1000)
println("end $delay")
}

The function must be modified by the keyword suspend because it calls the suspending function delay(); a suspending function can only be invoked from another suspending function, since they can only be executed from within a coroutine context.

The 3 calls pass a different delay for each execution, so each function will take a different time to complete.

Execution and output

Let’s run the base code and analyze the outputs, per the several println() calls in the code.

onCreate 1 Thread[main,5,main] => starts onCreate on the main thread

playWithCoroutines 1 Thread[main,5,main] => starts playWithCoroutines in the main thread
At this point launch is called to schedule the coroutine, but the coroutine does not start executing yet, because the main thread is still busy

Finished playWithCoroutines 4 Thread[main,5,main] => ends playWithCoroutines in the main thread

onCreate 2 Thread[main,5,main] => finishes onCreate in the main thread

onResume 1 Thread[main,5,main] => calls onResume in the main thread, as part of the Activity lifecycle

playWithCoroutines context [StandaloneCoroutine{Active}@ba2c414, Dispatchers.Main] => coroutine execution with this context. As it can be seen here, the job is the StandaloneCoroutine. Also important to notice this line is executed after the Activity lifecycle is completed

playWithCoroutines 2 Thread[main,5,main] => starts the coroutine execution in the main thread. Will then execute each suspending function sequentially, waiting for each one to end before starting the next, just like normal functions

execDelay 1 Thread[main,5,main] =>delays are executed sequentially, as determined by structured concurrency
start 10
end 10
execDelay 2 Thread[main,5,main]
start 12
end 12
execDelay 3 Thread[main,5,main]
start 15
end 15

playWithCoroutines 3 Thread[main,5,main] => ends the coroutine execution

The outputs clearly show how calling launch allows the onCreate function to continue execution, while the lambda block is suspended, waiting for execution when appropriate.

The coroutine framework takes care of all the scheduling and implementation details, while the code is written as normal sequential function calls.

Modified code — replacing the thread

Now let’s see what happens if the execution thread by using a different dispatcher. We’ll use now the IO dispatcher.

private val coroutineScope = CoroutineScope(Dispatchers.IO)

This dispatcher is a way to tell the system to use the I/O thread so operations will not block the UI.

onCreate 1 Thread[main,5,main]
playWithCoroutines 1 Thread[main,5,main]
Finished playWithCoroutines 4 Thread[main,5,main]
onCreate 2 Thread[main,5,main]
=> up to this point, execution is the same as using the Main dispatcher

playWithCoroutines context [StandaloneCoroutine{Active}@bde86fa, Dispatchers.IO] => notice the code now uses the IO dispatcher. Also notice that this line is now executed before the Activity lifecycle is completed; this is possible because the coroutine is executed in its own separate thread

playWithCoroutines 2 Thread[DefaultDispatcher-worker-1,5,main]
execDelay 1 Thread[DefaultDispatcher-worker-1,5,main]
start 10
=> the function is executed right away without waiting for the Activity lifecycle to complete

onResume 1 Thread[main,5,main]

end 10
execDelay 2 Thread[DefaultDispatcher-worker-1,5,main] => all calls are executed sequentially, but in the same thread worker-1

start 12
end 12
execDelay 3 Thread[DefaultDispatcher-worker-1,5,main]
start 15
end 15
playWithCoroutines 3 Thread[DefaultDispatcher-worker-1,5,main]

Running code concurrently

Now assume the code requires each call to execDelay() to run concurrently in its own thread. This is easily accomplished by replacing lines 26, 27 and 28 of the base code with the following lines:

 launch { execDelay(10, 1) }
launch { execDelay(12, 2) }
launch { execDelay(15, 3) }

What this code is doing is using the coroutine builder to create a separate coroutine for each call to execDelay(). Let’s see what the output in this case is:

onCreate 1 Thread[main,5,main]
playWithCoroutines 1 Thread[main,5,main]
Finished playWithCoroutines 4 Thread[main,5,main]
onCreate 2 Thread[main,5,main]

playWithCoroutines context [StandaloneCoroutine{Active}@bde86fa, Dispatchers.IO] => same as the previous execution

playWithCoroutines 2 Thread[DefaultDispatcher-worker-1,5,main] => notice this line shows the playWithCoroutines function runs in thread worker-1

execDelay 1 Thread[DefaultDispatcher-worker-2,5,main] => now it’s executed in its own thread worker-2
start 10

playWithCoroutines 3 Thread[DefaultDispatcher-worker-1,5,main]=> this call was always executed at the end; now it’s executed before the calls to execDelay() are finished because they are running in separate threads

execDelay 2 Thread[DefaultDispatcher-worker-3,5,main] => => now it’s executed in its own thread worker-3; all coroutines start at the same time

execDelay 3 Thread[DefaultDispatcher-worker-4,5,main] => => now it’s executed in its own thread worker-4
start 15
start 12
onResume 1 Thread[main,5,main] => Activity lifecycle executed completely asynchronously to the coroutine

end 10
end 12
end 15
=> now each call to execDelay() is executed in a separate thread, so their ends are grouped together

Using structure concurrency

Arguably one of the most useful features provided by coroutines, structured concurrency allows the code to run a number of concurrent threads, while waiting until all of them finish so the results can be aggregated.

To show how that works, we’ll modify the execDelay() function to return a number; we’ll also replace the launch coroutine builder with async, a coroutine builder that returns a Deferred value wrapped around the result from the lambda block, which can be used to wait for the result.

Here are the modifications:

    private fun playWithCoroutines() {
println("playWithCoroutines 1 ${Thread.currentThread()}")
coroutineScope.launch {
println("playWithCoroutines context ${this.coroutineContext}")
println("playWithCoroutines 2 ${Thread.currentThread()}")
val exec1 = async { execDelay(10, 1) }
val exec2 = async { execDelay(12, 2) }
val exec3 = async { execDelay(15, 3) }
val map = exec1.await() to exec2.await() to exec3.await()
println("playWithCoroutines sum = $map")
println("playWithCoroutines 3 ${Thread.currentThread()}")
}
println("Finished playWithCoroutines 4 ${Thread.currentThread()}")
}

Here is the new output:

onCreate 1 Thread[main,5,main]
playWithCoroutines 1 Thread[main,5,main]
Finished playWithCoroutines 4 Thread[main,5,main]
onCreate 2 Thread[main,5,main]
=> this block is the same as the previous execution

playWithCoroutines context [StandaloneCoroutine{Active}@bde86fa, Dispatchers.IO]
playWithCoroutines 2 Thread[DefaultDispatcher-worker-1,5,main]
execDelay 2 Thread[DefaultDispatcher-worker-3,5,main]
start 12
execDelay 1 Thread[DefaultDispatcher-worker-2,5,main]
start 10
execDelay 3 Thread[DefaultDispatcher-worker-4,5,main]
start 15
onResume 1 Thread[main,5,main]
end 10
end 12
end 15
=> this block is almost the same as previous execution, where each call to execDelay() runs concurrently in its own thread

playWithCoroutines map = ((kotlin.Unit, kotlin.Unit), kotlin.Unit)
playWithCoroutines 3 Thread[DefaultDispatcher-worker-3,5,main]
=> here is the main difference when compared to the previous case.
This line is now executed at the end, because the code is waiting for threads worker-2, worker-3 and worker-4 to end.
In other words, the worker-1 thread is now blocked waiting for those results.

With this approach, the execution was able to run execDelay() concurrently, while at the same time blocking the worker-1 thread until all threads finished executing. And all of that while writing the code as if it was executed sequentially.

That is a powerful feature of coroutines: no need to deal with threads, function synchronization, deadlocks, thread-safety and other relatively complicated features when working with concurrency.

Creating suspending functions with suspendCoroutine

In the examples so far, function execDelay() was declared with the suspend modifier because it was calling delay(), which is a suspending function.

However, that’s not always the case, and one still wants to declare a function as a suspending function.

One way of doing so is using the suspendCoroutine function. This function takes in a new parameter of class Continuation and a lambda block to be executed; once the lambda block finishes execution, the continuation resume() function can be used to indicate the suspended coroutine can continue.

Here is the modified function, where the delay() call was replaced by a for loop to emulate a long running operation:

    private suspend fun execDelay(delay: Long, index: Int) = suspendCoroutine {continuation ->
println("execDelay $index ${Thread.currentThread()}")
println("start $delay")
for (i in 1..(delay * 1000000)) {}
println("end $delay")
continuation.resume(Unit)
}

And here is the output:

onCreate 1 Thread[main,5,main]
playWithCoroutines 1 Thread[main,5,main]
Finished playWithCoroutines 4 Thread[main,5,main]
onCreate 2 Thread[main,5,main]
playWithCoroutines context [StandaloneCoroutine{Active}@bde86fa, Dispatchers.IO]
playWithCoroutines 2 Thread[DefaultDispatcher-worker-1,5,main]
execDelay 1 Thread[DefaultDispatcher-worker-3,5,main]
start 10
execDelay 3 Thread[DefaultDispatcher-worker-4,5,main]
start 15
execDelay 2 Thread[DefaultDispatcher-worker-2,5,main]
start 12
onResume 1 Thread[main,5,main]
end 15
end 10
end 12
playWithCoroutines sum = ((kotlin.Unit, kotlin.Unit), kotlin.Unit)
playWithCoroutines 3 Thread[DefaultDispatcher-worker-2,5,main]

This output is pretty much identical to the previous one, as one would expect. The calls to continuation.resume() inform the execution that the suspending function is finished execution and the suspended coroutine can resume execution.

Since the code used the async coroutine builder combined with the calls to the await() function, the coroutine will be resumed once all 3 threads are done.

Practical example

Before we end this article, here is the full code for a practical example. In the code, the application heeds to bind to a service — an action that is executed asynchronously — before it can invoke a method exposed by the service interface.

Before we end this article, here is a small real-life example on how to use coroutines to handle mixed asynchronous and synchronous calls.

IN this example, the application must bind to a service — an asynchronous operation — and wait before it can call a method exposed by the service interface (normally defined via an AIDL file).

To simplify the example, the code uses GlobalScope, but it can use any other suitable coroutine scope.

The bound service is assumed to return a Messenger, which can then be used by the app to send data to the service.

Since this is an example, exception and null pointers handling were omitted.

package com.example.coroutineexample

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.os.Message
import android.os.Messenger
import androidx.activity.ComponentActivity
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine

class MainActivity : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch(Dispatchers.IO) {
val messenger = bindToService(applicationContext)
messenger.send(Message.obtain())
}
}

private suspend fun bindToService(context: Context): Messenger =
suspendCoroutine { continuation ->

val serviceIntent = Intent("com.start.some.service").apply {
setClassName("com.some.service", "com.some.service.MainService")
}
context.bindService(
serviceIntent,
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val serviceInterface = Messenger(service)
continuation.resumeWith(Result.success(serviceInterface))
}

override fun onServiceDisconnected(name: ComponentName?) {
TODO("Not yet implemented")
}
},
Context.BIND_AUTO_CREATE)
}
}

Analysis

  • Making bindToService suspending, the code stops execution and waits until the function notifies it is completed, by means of continuation.resumeWith()
  • The key here is to use the continuation instance inside the ServiceConnection in order to allow the code to coninue execution
  • Using Dispatchers.IO allows the coroutine to be executed in a separate thread from the UI thread, unblocking onCreate

Conclusion

Using the base code and its variants, this article covered some of the core concepts when using coroutines, and hopefully how to properly use them.

The article covered how to use:

  1. Coroutine scope
  2. Coroutine context
  3. Coroutine builder
  4. Dispatchers
  5. Suspending functions
  6. Suspending point

With this foundation, it shall be easier to understand other functions and mechanics within coroutines, like Flows and Channels, as well as other functions used when creating suspending functions like withContext and withTimeout; it will also help to understand the lifecycle-aware scopes used in Android.

References

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Mauricio Andrada
Mauricio Andrada

Written by Mauricio Andrada

20+ years of experience with software development in the Telecommunications industry; currently DMTS at Verizon working on applications for 5G, AI and MEC

No responses yet

Write a response

Recommended from Medium

Lists

See more recommendations