Use suspendCoroutine to connect callbacks and coroutines

Whenever we need to write asynchronous code we tend to use callbacks which allow us to trigger an action and, instead of waiting for it to finish, get notified through the callback for the action’s completion. Coroutines change that and help us write asynchronous code but in a sequential way.

This means that instead of writing code like this:

fun main() {
downloadTasks(
object : DownloadCallback {
override fun onDownloaded(tasks: List<Task>) {
printTasks(tasks)
}
}
)
}
interface DownloadCallback {
fun onDownloaded(tasks: List<Task>)
}
private fun downloadTasks(callback: DownloadCallback) {
println("Downloading…")
// code that makes a network call and returns the list of tasks
callback.onDownloaded(listOf(Task(1), Task(2), Task(3)))
}
private fun printTasks(tasks: List<Task>) {
tasks.forEach { task -> println(task.id) }
}

we can write it like this:

fun main(): Unit = runBlocking {
launch {
val tasks = downloadTasks()
printTasks(tasks)
}
}
private suspend fun downloadTasks() {
println("Downloading…")
// code that makes a network call and returns the list of tasks
return listOf(Task(1), Task(2), Task(3))
}
private fun printTasks(tasks: List<Task>) {
tasks.forEach { task -> println(task.id) }
}

But what do we do when there is no easy way to remove callbacks from existing code or when we use a third party library that is not coroutines ready? This is where suspendCoroutine comes to save the day.

suspendCoroutine

suspendCoroutine is a function that does exactly what is says. It suspends the coroutine that it was called from and provides a way to resume it.

Lets have an example. The code here:

fun main(): Unit = runBlocking {
launch {
print("1 ")
print("2 ")
print("3 ")
print("4 ")
println("Done!")
}
}

simply prints 1 2 3 4 Done!. If we change it to:

fun main(): Unit = runBlocking {
launch {
print("1 ")
print("2 ")
print("3 ")
suspendCoroutine<Unit> { }
print("4 ")
println("Done!")
}
}

it will print 1 2 3 and then it will just wait. We suspended the coroutine but we did not resume it. To do so we will use the continuation instance that suspendCoroutine provides:

fun main(): Unit = runBlocking {
launch {
print("1 ")
print("2 ")
print("3 ")
suspendCoroutine<Unit> { continuation ->
print("… ")
continuation.resume(Unit)
}
print("4 ")
println("Done!")
}
}

now it prints 1 2 3 … 4 Done!. The coroutine printed the first three numbers, got suspended, while being suspended another block of code got executed and printed the dots and then resumed the coroutine allowing it to print the final number and done.

Continuation adapter

Back to our first example. Lets say that downloadTasks cannot be changed. We still need to call it and provide a callback for its results.

What we need to do is to suspend the coroutine, call downloadTasks to.. well.. download the tasks and provide a callback that upon completion it will resume the coroutine with the tasks at hand.

To achieve that we first need to create an adapter that will connect the callback with a continuation:

private class ContinuationAdapter(
private val continuation: Continuation<List<Task>>
) : DownloadCallback {
override fun onDownloaded(tasks: List<Task>) {
continuation.resume(tasks)
}
}

and then call suspendCoroutine:

fun main(): Unit = runBlocking {
launch {
val tasks = suspendCoroutine<List<Task>> { continuation -> downloadTasks(ContinuationAdapter(continuation)) }
printTasks(tasks)
}
}

That’s it. The adapter resumes the coroutine by providing the tasks that are then returned to the suspension point.

One more thing

Along side suspendCoroutine there is also suspendCancellableCoroutine which provides a cancellable continuation. That means that in addition of resuming we can also execute code upon cancellation:

fun main(): Unit = runBlocking {
val job = launch(Dispatchers.IO) {
val tasks = downloadAllTasks()
printTasks(tasks)
}
delay(100)
job.cancel()
}
private suspend fun downloadAllTasks(): List<Task> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { print("Cancelled…") }
downloadTasks(ContinuationAdapter(continuation))
}
}
private class ContinuationAdapter(
private val continuation: Continuation<List<Task>>
) : DownloadCallback {
override fun onDownloaded(tasks: List<Task>) {
continuation.resume(tasks)
}
}
interface DownloadCallback {
fun onDownloaded(tasks: List<Task>)
}
private fun downloadTasks(callback: DownloadCallback) {
println("Downloading…")
sleep(150) // simulate network latency
val tasks = listOf(Task(1), Task(2), Task(3))
callback.onDownloaded(tasks)
}
private fun printTasks(tasks: List<Task>) {
tasks.forEach { task -> println(task.id) }
}
to improve the readability of the code we can hide the suspension inside another function

this will print Downloading… and then Cancelled…

Leave a comment