Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Third Edition · Android 12 · Kotlin 1.6 · Android Studio Bumblebee

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

5. Async/Await
Written by Filip Babić

So far you’ve seen how coroutines and suspendable functions can be used to bridge threads and execute asynchronous work that doesn’t add much overhead to your program. You also saw how you can migrate from callback-based APIs to ones that are coroutine-based, which has the signature of a regular function returning the value you need when called. These functions were actually blocking but could have been asynchronous.

In this chapter, you’ll see how you can build similar mechanisms which aren’t blocking and can work asynchronously and in parallel. They can also return values, as if you’re calling a standard function. Sounds too good to be true? Well you’ll see how all of this functionality is actually an old concept, so let’s get going!

The Async/Await Pattern

One of the biggest problems in computing is being able to return values from asynchronous functions. Even more so, if you’re calling a function that creates a different thread to run in, you can’t return a value to the outer function. This is a program restriction because the system doesn’t know when to return and has a hard time bridging between threads. But there is a way to achieve this behavior with the async/await pattern.

It’s a very simple idea: Build a wrapper around the value you need, then call a function that provides the value and passes it to the wrapper. Once it’s ready, you can request it. This goes all the way back to queues, as the wrapper may as well be a simple class that holds a queue of the capacity of one item.

Once you request the value, you suspend the function you requested it in, until the data shows up. This type of mechanism works and has been tried and tested through time. A very similar implementation exists even in the Java API — in the form of futures. However, if you’re coming from a Javascript background, you’re probably familiar with promises which take a similar approach but execute them differently.

Learning From the Past

Sometimes, it’s best to take a long, hard look at the past to see what you can learn and maybe use to achieve your goals. When coroutines were designed, the team writing the API did just that. That isn’t surprising, given that the concept of coroutines is decades old. More specifically, they looked to the future and promise patterns. Each of the patterns has a specific syntax and way of dealing with asynchronously provided values. Let’s see what they’re really about.

Promising Values

A promise construct is just what the names states — a promise of a value, which might or might not be there at all. The value is promised to surface at some point in time for you to consume, but sometimes things break. This is why the promise also allows you to handle any errors that happen along the way.

Promises work by taking a function call and storing it in a construct. That alone doesn’t do much, but the key to a promise is that you can chain them indefinitely. Once you create your first one, you can chain the next promise call, which will take the input from the previous one. So, if your first promise returns a String, you can use that value in the next call — to turn it into an Int, for example. Then, in the third call, you’d get an Int, and so on.

Promises rely on two function calls: then and catch. then takes in the currently promised value, and allows you to either map it to something else or just consume it. catch is the fallback function, catching any errors that happen along the way and allowing you to act upon it. However, there has to be at least one catch clause. A standard promise chain would look like this:

database
  .findOne({ email: request.email })
  .then(user => {
    if (!user) {
      // the user doesn't exist, so let's create it
      return service.registerUser(request.data)
    } else {
      return null
    }
  })
  .then(registerStatus => {
    // do something after registration
  })
  .catch(error => {
    // handle error
  })

This code would try to register a user, if the user doesn’t exist in the database already. In the case that it does exist already, you’d return null, or undefined, and then handle it further. If anything bad happens, you can catch it in the catch clause of the promise chain.

Promises look really clean and straightforward, and they are easy to use and learn, but there are caveats. You can’t really return a value from a promise; you can only return a promise, since the value might not be there. And you have to rely on the call-chain structure. This makes you rely on promises entirely, which makes sense for web applications, which might not rely on multiple threads.

But, for Android applications or similar, which rely on multithreading, this is a limitation. Since you usually want to do some background processing in modern applications, but need to consume these values on the main thread, promises won’t work since they are bound to a single thread.

Additionally, if you have multiple function flows, you have to handle them within the original promise. Since you are returning the response to the user in the outer-most promise, you have to propagate all the cases to the internal promise somewhere down the line. This tends to be clunky and tends to require a lot of utility classes, just to bury the excess code. The worst part of promises is that, if you forget to return values from one of the chained calls, the entire lower part of the chain won’t be able to continue, since it won’t have any values to consume.

A different approach, yet based on the same principles, is the future pattern. Let’s see how these two are different.

Thinking About the Future

Futures sound a lot like promises, and they behave similarly. When you create futures, you’re not promising that a value will be there somewhere along the line, since promises are easy to break. You explicitly say that this value will exist, or you have to face some consequences. In promises, it’s easy to break things: just miss a return call and your entire chain won’t have anything to consume, in turn freezing the entire function call.

In futures, you have to declare a function that has a return statement; otherwise, you get a compile-time error. Additionally, once you create a future, you can check its status at any point in time, using isDone. If isDone returns true, you’re ready to use your value.

Once you’re ready to use the value, all you have to do is call get to get it. The internal part of futures is really fun to analyze. Futures use something called an executor to run their tasks. They handle the threading and execution of the tasks in each of the futures you create and you can achieve things like parallelism, using thread pools. You’ll learn more about executors, scheduling and thread pools in “Chapter 7: Context Switch & Dispatching”.

Since Java-based APIs don’t have the concept of suspending, every call you make on a Future will be blocking. As such, calling get right away might in turn block off your main thread for a long time. Let’s see how futures work and what they look like:

private static ExecutorService executor = 
      Executors.newSingleThreadExecutor();

public static Future<Integer> parse(String input) {
  return executor.submit(() -> {
    Thread.sleep(1000)
    return Integer.parseInt(input);
  });
}

This snippet of code will create an executor, which uses a new thread to do its business. If you call parse, with a String, it will return a future, which will wait for a second and then return the Integer value of the String. To call this code you’d have to do the following:

public static void main(String...args) {
  Future<Integer> parser = parse("310");

  while(!parser.isDone()) {
    // waiting to parse
  }

  int parsedValue = parser.get();
}

You create a future and it begins to execute the task in the thread the executor created. Once your call to isDone returns true, you know it’s ready to use. Finally, by calling get, you receive the value from the Future, which is now cached within the object itself and can be reused.

This is far more flexible than promises, but it also suffers from the problem of having to wait for the value, either by blocking with get or running a while loop until the future is done - which is blocking as well.

Futures are great when you need to process and produce values in different threads. If you want to achieve parallelism, you can create multiple threads for your futures to use and run multiple tasks at the same time.

They also allow you to have clean control flow logic, since the values produced can be used in sequential code, and don’t have to rely on callbacks or chained function calls. But, on the other hand, their values are always received in a blocking way, and as such can be expensive to wait for when you have a user interface to render.

Let’s see the key differences in these approaches and which one async/await is more similar to.

Differentiating Approaches

The key characteristic that distinguishes promises from async/awaits is that promises rely on chains of function calls, sort of like the builder pattern, but ultimately promises are a series of callbacks. Using promises is very similar to reactive extensions, which operate on streams of values. You could, for example, chain transforming operators or delay the data being processed.

This code will look very structured at first, but if you need to have multiple flows or logical paths, you’ll end up having staircases of nested promises. For this reason, promises can be tedious and ugly to work with. Newer versions of Javascript allow you to use the async and await keywords, which work as promises at a lower level but hide that boilerplate away from you.

Futures and async/await, however, rely on having a single value primed for usage, burying it down underneath various design patterns and constructs. This also allows you to use their results as values so that you can write sequential code that doesn’t use callbacks but suspends the code waiting for their values. They are both fantastic mechanisms when you need clean and understandable code without nested functions or callbacks.

However, by eagerly waiting for values, you risk freezing the UI if you don’t pay attention to threading and you even risk creating a deadlock, since your function may not return the value at all while you’re waiting for the result. The difference between futures and async/await is that futures rely on blocking calls, which will definitely freeze up the thread you are currently in.

Conversely, the async/await pattern relies on suspending functions, like the Kotlin Coroutines API. As such, it alleviates the need to block code, but it does have caveats as well.

Let’s see how async and await are implemented in the Coroutines API, how to use them to your advantage and which mechanisms exist to stop you from breaking your code or your program in case the data just doesn’t show up.

Using Async/Await

To follow the code in this chapter, open this chapter’s starter project, using IntelliJ, selecting Open and navigating to the async-await/projects/starter folder, selecting the async_await project.

Right about now, you’re probably wondering how the async/await pattern works in the Kotlin Coroutines API. Very close to the future pattern, async in Kotlin returns a Deferred<T>. Just like you would have a Future<T>, the deferred value just wraps a potential object you can use. Once you’re ready to receive the object, you have to await for it, effectively requesting the data, which might or might not be there. If the data is already provided and delivered, the call will turn into a simple get; otherwise, you’re code will have to suspend and wait for the data to come to the wrapper.

Quite simply, it’s as if you’re creating a BlockingQueue instance, with the capacity for a single value. And, at any point in time, you can attempt to get the value or suspend the code while waiting for it. The key difference is that you’re not actually blocking threads, but are instead suspending code. It’s time for you to see how it’s all done backstage using coroutines.

The pattern is called async/await for a reason — because the full implementation requires two function calls — async, to prepare and wrap the values, and await, to request the value for use. Let’s see what’s in the signature of both of these functions, before you jump into using this approach.

If you open up the async definition, you can see the following code:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

When you call async, you can pass it a CoroutineContext to bind it to a certain Job or a Dispatcher. You can also start it in different modes with the CoroutineStart parameter. But, most importantly, you have to pass in a lambda block, which has access to the CoroutineScope that you called the function in and needs to return a value it will try to store in the Deferred. It does so by creating a new DeferredCoroutine, which it starts with the block lambda, and returns the aforementioned coroutine. You’ll learn a bit more about this coroutine in the next section.

This function call basically wraps the value in a coroutine, which implements the Deferred<T> interface on which you will call await later on. Once you call await, the coroutine will try to produce the value for you or suspend until it’s there. Let’s check out the await signature, to see if there’s anything interesting there:

/**
* Awaits for completion of this value without blocking a thread
* and resumes when deferred computation is complete,
* returning the resulting value or throwing the
* corresponding exception if the deferred was cancelled.
*
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled
* or completed while this suspending function is waiting,
* this function immediately resumes with [CancellationException].
*
* This function can be used in [select] invocation 
* with [onAwait] clause.
* Use [isCompleted] to check for completion of this
* deferred value without waiting.
*/
public suspend fun await(): T

The function itself is extremely simple, but the idea behind it is genius. Instead of actually blocking a thread, you can suspend the entire function or coroutine and just have it resolve after the value is ready. It’s time to use this approach and migrate a currently blocking snippet of code, to async. If you haven’t already, open the starter project from this chapter, under the async-await, projects folder. Next, open up AsyncAwait.kt, and you should see the following code:

fun main() {
  val userId = 992
  
  getUserByIdFromNetwork(userId) { user ->
    println(user)
  }
}

private fun getUserByIdFromNetwork(userId: Int, onUserReady: (User) -> Unit) {
  Thread.sleep(3000)

  onUserReady(User(userId, "Filip", "Babic"))
}

data class User(val id: Int, val name: String, val lastName: String)

This piece of code tries to get a User from a simulated network call and print it out. The problem, though, is that it’s calling Thread.sleep, which halts the execution for three seconds. Just as if you had a blocking network call, you have to wait for the data to come back, before you can use it. To pour a little salt on the wound, it’s also using a callback to pass the data back to the caller once it’s ready. Next, you’ll refactor this to use async and await from the Coroutines API.

Change the getUserByIdFromNetwork code to the following:

private fun getUserByIdFromNetwork(userId: Int) = GlobalScope.async {
  println("Retrieving user from network")
  Thread.sleep(3000)

  User(userId, "Filip", "Babic")
}

Be sure to import GlobalScope and async:

import kotlinx.coroutines.*

First, you’ve removed the callback from the parameters, and, secondly, you’ve returned a GlobalScope.async block as the return value for the function. Now, the function returns a value and it doesn’t rely on the callbacks to consume it.

Finally, you have to update the code in main() to use the new version of getUserByIdFromNetwork():

fun main() {
  val userId = 992
  val userData = getUserByIdFromNetwork(userId)
  
  println(userData)
}

Now, if you run the code above, you’ll see a similar output to this:

DeferredCoroutine{Active}@6eee89b3
Retrieving user from network

This is because getUserByIdFromNetwork now returns a Deferred<User>. In order to get the user, you have to call await on it. But await is a suspendable function and to call it you have to be in another suspendable function or coroutine builder, so you have to wrap the code above in a launch. Change main to:

fun main() {
  val userId = 992

  GlobalScope.launch {
    val userData = getUserByIdFromNetwork(userId)

    println(userData.await())
  }

  Thread.sleep(5000)
}

The flow is similar to the earlier code, except that you’re also calling await to return the User in order to print it out. You’ve successfully migrated callback-based code to the async/await pattern!

Walking through all of the changes and what the resulting code does:

  • First, you removed the callback from the function, since you’ll be returning a value.
  • Then, you had to return the result of async and the User from within the lambda block.
  • Finally, you had to wrap await in a coroutine, since it’s a suspendable function.

These three steps are everything you need to do to migrate your code to async/await.

Now, how it works is a another thing. As mentioned before, it creates a coroutine and masks it with the Deferred<T> value. Through the interface, you have access to the value, since the interface exposes await.

Once you call await, you’re suspending the function call, effectively avoiding blocking the thread. You then wait for the lambda block to execute in order to use the value that is stored internally. When the value is ready, the function stops being suspended and your code continues normally.

Now, a lot of the work here is determined by the Deferred type, so let’s see what it actually is.

Deferring Values

Every async block returns a Deferred<T>. It’s the core mechanism that drives that piece of the Coroutines API and it’s very important to understand how it works.

async creates a DeferredCoroutine or a LazyDeferredCoroutine. Such coroutines have a generic inference and also implement the Continuation<T> interface, allowing the interception of execution flow and passing the values all the way up to the call site, just like with suspendCoroutine. This is similar to how the future pattern works, which you’ve seen before.

Once the coroutine is created, unless its CoroutineStart is CoroutineStart.LAZY, it will launch immediately. The code will start to execute in the thread that you declared with the context parameter using Dispatchers. Once the code finishes executing and produces a value, it will be stored internally. If, at any point in time, you call await, it will be a suspended call, which will create a new continuation and execution flow, waiting until the value is ready for use. If it isn’t ready, the function will have to wait. If the value is already provided, you’ll get it immediately.

You can also check the status of a deferred value, since it also implements the Job interface. You can check flags like isActive and isCompleted to find out about its current lifecycle state. You also have a few nifty functions, which you can use to receive a value or an exception if the Job was canceled. These functions are getCompleted, which returns the value — or an exception if the deferred value was canceled — and the getCompletionExceptionOrNull, which returns the CancellationException or null, if the Job was not canceled. Using those functions, you can also check the details around the completed state of the deferred value.

So a good way to explain what a Deferred is, is that it’s a Job with a result. The job can run in parallel, it may or may not produce a resulting value and it can be canceled and joined with other jobs. This provides a powerful API you can bend to your will. One of the ways you can utilize deferred values is by combining them together to call functions with multiple parameters.

Combining Multiple Deferred Values

Being able to create deferred values, which are built in the background but could be accessed on the main thread in one function call, is an amazing thing. But the real power of async is being able to combine two or more deferred values into a single function call. Let’s see how to do so.

So far, you’ve worked with an example that mocked a network request, but it’s time to expand on that example. You might have noticed users.txt in the project. It’s a file containing 13,000 lines of text. Most lines contain the information required to build Users — an id, a name and a last name. Some of the lines are empty, some don’t have all three items and some are just plain gibberish. The idea behind this is to read the entire file, parse and split each line and create users out of them. After that, you’ll use the list of users and the user you got from the mocked network call to see if that user is stored in the file.

Having this will allow you to see how two deferred values can be primed and used in a single function call. Now, navigate back to AsyncAwait.kt. Once there, add the following code to the file:

private fun readUsersFromFile(filePath: String) =
    GlobalScope.async {
      println("Reading the file of users")
      delay(1000)

      File(filePath)
          .readLines()
          .asSequence()
          .filter { it.isNotEmpty() }
          .map {
            val data = it.split(" ") // [id, name, lastName]

            if (data.size == 3) data else emptyList()
          }
          .filter {
            it.isNotEmpty()
          }
          .map {
            val userId = it[0].toInt()
            val name = it[1]
            val lastName = it[2]

            User(userId, name, lastName)
          }
          .toList()
    }
    
private fun checkUserExists(user: User, users: List<User>): Boolean {
  return user in users
}

Also make sure to add the following imports:

import java.io.File

This function does everything described above in async and will return a Deferred that holds a list of users. Now, you can tweak the main code to look like the following:

fun main() {
  val userId = 992

  GlobalScope.launch {
    println("Finding user")
    val userDeferred = getUserByIdFromNetwork(userId)
    val usersFromFileDeferred = readUsersFromFile("users.txt")

    val userStoredInFile = checkUserExists(
      userDeferred.await(), usersFromFileDeferred.await()
    )

    if (userStoredInFile) {
      println("Found user in file!")
    }
  }

  Thread.sleep(5000)
}

In the above:

  • You create a userDeferred by calling getUserByIdFromNetwork. This will set off the block of code which waits three seconds to return a user.

  • You then prime usersFromFileDeferred that will return a list of users which are stored in users.txt.

  • Once both of the values are primed, you can call checkUserExists to see if the user you received from the network call matches any user in the list loaded from the file.

  • checkUserExists takes in two parameters: a user you need to look up and a list of users in which to look them up.

  • To pass in those two values, you have to await for the Deffered results.

  • Finally, if the user is stored in the file, you print out: “Found user in file!”.

The arguments you pass to the function are the await results from both of the deferred values you prepared earlier and you have a line of code that suspends the function, waiting for the two values.

This is the right way to use multiple deferred values. You’re effectively creating a single suspension point in your program, which suspends two functions. Another thing you could do is have checkUserExists suspendable and then await from within, but it’s more convenient to await the values before passing them further to other functions.

If you run this code, you’ll see the following output immediately:

Finding user
Retrieving user from network
Reading the file of users

And after approximately three seconds, you should see Found user in file!. This is because the coroutines that were created around the Deferred aren’t lazy, and they fire off right away — but only when you await for the values do you suspend the code. After calling await, you pass the received values to checkUserExists and then you receive the output that the user exists in the file.

Using this approach, you can combine any number of deferred values and achieve smart and simple parallelism, which isn’t built upon the concept of callbacks of streams of data. With that in mind, the code is extremely easy to understand, since it resembles sequential, synchronous code, even though it might be fully asynchronous behind the curtains. This is the true power of coroutines and the async/await pattern.

Remember how in the previous chapter, you used withContext to achieve the same behavior but with just one value. This is the key difference between the two patterns — withContext is best used when you need to return a single value from a suspendable piece of code, whereas async/await are best used when you have multiple functions running in parallel combined to produce one result.

However, there is one thing about this code that isn’t ideal: the fact that this code isn’t that well structured when it comes to cancellation and resource management. Let’s see how you can polish that code to perfection by following Jetbrains’ ideology.

Being Cooperative and Structured

The above examples of code are pretty well built and they serve the purpose of explaining how you can prime multiple values in parallel and pass them to functions once they are ready. On the other hand, the problem is if something goes wrong or the async block isn’t built properly, you’re going to block a thread and potentially freeze the entire system. For example, if your async block contains a while loop and the condition doesn’t have a break strategy, your function will never return the value. Moreover, if you build functions that do heavy operations and take a lot of time to finish, you should be able to cancel them at any point in their execution. Otherwise, you risk canceling their parent Job, but not the job itself. Then you’ll end up with code that is still running and using up resources, even though its Job has been canceled.

This is why your code should always be cooperative.

Some time after releasing coroutines, Jetbrains began to see various kinds of usage and libraries being built around them. Jetbrains then released an article clarifying some details, explaining how the initial examples and ideas people had about coroutines weren’t 100% correct. Take an expanded example of the function you used above:

private suspend fun getUserByIdFromNetwork(userId: Int) = GlobalScope.async {
  println("Retrieving user from network")
  delay(3000)
  println("Still in the coroutine")

  User(userId, "Filip", "Babic") // we simulate the network call
}

If you call this function, this simple snippet will return a User after three seconds. Since it’s a suspend function, you need to wrap it in a coroutine builder, like so:

fun main() {
  GlobalScope.launch {
    val deferredUser = getUserByIdFromNetwork(130)

    println(deferredUser.await())
  }
}

But what happens if you cancel the Job from the launch() after its block starts executing? The getUserByIdFromNetwork() would still run for three seconds and return a value, even though the parent job is canceled. This causes a waste of time and resources in computing, which would be better spent elsewhere. Or at least it did with the initial release.

This is why Jetbrains came up with the idea of structured concurrency and cooperative code. The idea is to write code that reflects upon the state of its caller and to build coroutines that rely on their parent’s state. In simple terms, if the parent job is canceled, so should its children. By default, this behavior does exist in the Coroutines API, as you’ve seen.

But you could still write code that doesn’t enforce this behavior. If you tweak the snippet above to the following, for example:

fun main() {
  val launch = GlobalScope.launch {
    val dataDeferred = getUserByIdFromNetwork(1312)
    println("Not cancelled")
    // do something with the data

    val data = dataDeferred.await()
    println(data)
  }

  Thread.sleep(50)
  launch.cancel()

  while (true) { // stops the program from finishing
  }
}

You’ll see that getUserByIdFromNetwork() executes completely, even though you’ve canceled the coroutine it was called in. This is because the snippet above isn’t cooperative, and doesn’t allow the code to suspend and cancel if needed. It doesn’t care about the place of origin or the scope and context of its parent. The right way to build this function is the following:

private suspend fun getUserByIdFromNetwork(
    userId: Int,
    parentScope: CoroutineScope) =
    parentScope.async {
      if (!isActive) {
        return@async User(0, "", "")
      }
      println("Retrieving user from network")
      delay(3000)
      println("Still in the coroutine")

      User(userId, "Filip", "Babic") // we simulate the network call
}

The function now takes in the parent CoroutineScope instance as a parameter and launches an async block from there. Furthermore, it checks isActive from the parent job so that it doesn’t necessarily proceed with execution if the parent turns out to be canceled. If you launch main from above again, passing in the parent scope, the code will only print out Retrieving user from network and Not Cancelled, after which it won’t proceed to wait three seconds to return a value to a coroutine that has been terminated.

Finally, the code for combining two values could be improved as well. If you examine the example from before:

GlobalScope.launch {
  val userDeferred = getUserByIdFromNetwork(userId)
  val usersFromFileDeferred = readUsersFromFile("users.txt")

  println("Finding user")
  val userStoredInFile = checkUserExists(
      userDeferred.await(), usersFromFileDeferred.await()
  )

  if (userStoredInFile) {
    println("Found user in file!")
  }
}

You might see the same problem as before. You’re priming two deferred values and using them right below without checking for the current state of the job. Since this is launched at a global scope, unless you store the returning Job somewhere, to cancel it later on, you will be effectively creating a fire-and-forget function, which might take up valuable resources. You probably see a pattern here. You should always have control of jobs you start and the coroutine scope you start them in. Coroutine scopes bind coroutines to the lifecycle of an object. This is why you shouldn’t really use GlobalScope extensively, but provide custom scopes instead.

But how do you assure the CoroutineScope you’re using for starting coroutines is correct? By implementing the interface yourself!

The guide for cooperative code and structure concurrency states that you should confine your coroutines to objects with a well-defined lifecycle, like the Activity in an Android application environment. So let’s implement the CoroutineScope in a dummy class. Create a new Kotlin class, and name it CustomScope.kt. Change the code within it, to the following:

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

class CustomScope : CoroutineScope {

  private var parentJob = Job()

  override val coroutineContext: CoroutineContext
    get() = Dispatchers.Main + parentJob
}

Once you implement the interface, you have to provide a CoroutineContext, so that the coroutines have a context to run with. The suggested implementation is a combination of a Dispatcher, for default threading and a Job to bind the coroutine lifecycle. This is a decent default implementation, but it could be better. Add the following functions to the class:

fun onStart() {
  parentJob = Job()
}

fun onStop() {
  parentJob.cancel()
  // You can also cancel the whole scope
  // with `cancel(cause: CancellationException)`
}

By adding this functionality, you can stop and cancel all the coroutines started with this scope, and by starting it, you can create a new job the coroutines will depend on for their lifecycle and possible isActive checks. Once you call onStop, all of the coroutines will be cancelled, and if you’ve implemented them to be cooperative, they shouldn’t take up any resources. Now, if you want to start coroutines with the new scope, all you have to do is the following:

fun main() {
  val scope = CustomScope()

  scope.launch {
    println("Launching in custom scope")
  }
  
  scope.onStop() //cancels all the coroutines
}

Pretty useful and rather clean! Let’s apply it to the previous example, where you looked up a user in a file. Apply the following change to main:

fun main() {
  val scope = CustomScope()

  val userId = 992

  scope.launch { // launching using the scope
    println("Finding user")
    val userDeferred = getUserByIdFromNetwork(userId, scope) // pass in scope
    val usersFromFileDeferred = readUsersFromFile("users.txt", scope) // pass in scope

    val userStoredInFile = checkUserExists(
      userDeferred.await(), usersFromFileDeferred.await()
    )

    if (userStoredInFile) {
      println("Found user in file!")
    }
  }

  /**
   * Cancels all the coroutines. You should only see "Finding user" printed out before the program ends.
   */
  scope.onStop()
}

As you can see, instead of using GlobalScope, you’ll use the custom scope you created to make the code responsive to cancellation events. Now do the same for the two data fetching functions:

private suspend fun getUserByIdFromNetwork(
    userId: Int,
    scope: CoroutineScope // Expose the scope as a parameter
) = scope.async { // Apply the scope
  // The rest of the code...
}

// Apply the same changes
private fun readUsersFromFile(
    filePath: String,
    scope: CoroutineScope
) = scope.async {
  // The rest of the code...
}

In this case, you’re just exposing the scope as a parameter and you’re using it to launch the data fetching operations. Build and run. The output of the code should be the following:

Finding user

And nothing else! Because you are using a custom scope that you can cancel manually, to start the coroutines, you don’t have to worry about operations running after any cancel events.

The code is now optimized, as it closes the coroutines as soon as it can do so after you call cancel on the CoroutineScope. Usually, this means it will cancel the coroutines at the first suspension point, or the first check of isActive.

The second problem you want to solve is producing async blocks that might never return a value. Say you have the following code:

fun <T> produceValue(scope: CoroutineScope): Deferred<T> =
    scope.async {
      var data: T? = null

      requestData<T> { value ->
        data = value
      }

      while (data == null) {
	     // dummy loop to keep the function alive
      }

      data!!
 }

This is a function that allows you to provide any type of value through async by wrapping an existing function that uses callbacks. The implementation of requestData is not important, just think of it as a go-to function to fetch any type of data and pass it to you, using a callback.

The code will work as expected if nothing goes wrong. But if the callback is never triggered and you’re calling the function this way:

fun main() {
  GlobalScope.launch(context = Dispatchers.Main) {
    val data = produceValue<Int>(this)

    data.await()
  }
}

It could ultimately suspend the launch block indefinitely; halting the function. Even if you cancel the Job returned from launch, if await has been called with the Unconfined or Main dispatcher, you could freeze the system and the user interface. This is why you should again integrate the parent Job’s isActive flag as the key condition breaker. If you change the code to the following:

fun <T> produceValue(scope: CoroutineScope): Deferred<T> =
    scope.async {
      var data: T? = null

      requestData<T> { value ->
        data = value
      }

      while (data == null && scope.isActive) {
        // loop for data, while the scope is active
      }

      data!!
    }

You rely not only on your internal-function condition, but also scope.isActive, allowing you to cancel the outer-most coroutine and the async block, as well. Note that the Jetbrains team has added better cancellation with coroutines, so the freezing of the UI shouldn’t happen, but better to be safe than sorry. However, the cancellation sometimes cannot be propagated. If your code doesn’t create a suspension point in which it can be cancelled, then you’re just calling regular, blocking code and you can still block the thread the coroutine is in.

So, be cooperative with your code!

Note: This piece of code is just representative and is not available in the projects, as requestData and produceValue are highly specific and are based on the way you fetch data within your apps. It could use a database, backend client, file system or other sources of information to provide the data the user requires.

But it still serves as a good example of how even retry or loop based code can be cooperative and can rely on the CoroutineScope to know when it needs to be canceled.

Key Points

  • The async/await pattern is founded upon the idea of futures and promises, with a slight twist in the execution of the pattern.

  • Promises rely on callbacks and chained function calls to consume the value in a stream-like syntax, which tends to be clunky and unreadable when you have a lot of business logic.

  • Futures are built with tasks, which provide the value to the user, wrapped in a container class. Once you want to receive the value, you have to block the thread and wait for it or simply postpone getting the value until it is ready.

  • Using async/await relies on suspending functions, instead of blocking threads, which provides clean code, without the risk of blocking the user interface.

  • The async/await pattern is built on two functions: async to wrap the function call and the resulting value in a coroutine, and await, which suspends code until the value is ready to be served.

  • In order to migrate to the async/await pattern, you have to return the async result from your code, and call await on the Deferred, from within another coroutine. By doing so, you can remove callbacks you used to use, to consume asynchronously provided values.

  • Deferred objects are decorated by the DeferredCoroutine. The coroutine also implements the Continuation interface, allowing it to intercept the execution flow and pass down values to the caller.

  • Once a deferred coroutine is started, it will attempt to run the block of code you passed, storing its result internally.

  • The Deferred interface also implements the Job interface, allowing you to cancel it and check its state — the isActive and the isCompleted flags.

  • You can also handle errors a deferred value might produce, by calling getCompletionExceptionOrNull, and checking if the coroutine ended with an exception along the way.

  • By returning Deferreds from function calls, you’re able to prime multiple deferred values, and await them all in one function call, effectively combining multiple requests.

  • Always try to create as few suspension points in your code as possible, making the code easier to understand.

  • Writing sequential, synchronous-looking code is easy using the async/await pattern. It makes your codebase clean and requires less cognitive load to understand the business logic.

  • You can write coroutine-powered code in a bad way. Doing so, you might waste resources or block the entire program. This is why your code should follow the idea of structured concurrency.

  • Being structured means your code is connected to other CoroutineContexts and CoroutineScopes, and carefully deals with threading and resource management.

  • Always try to rely on safe CoroutineScopes, and the best way is by implementing them yourself.

  • When you implement CoroutineScope, you have to provide a CoroutineContext, which will be used to start every coroutine.

  • It’s useful to tie the custom CoroutineScope to a well-established lifecycle, like the Android Activity.

  • It’s important to write cooperative code as well, which checks the isActive state of the parent job or scope to finish early, release resources, and avoid the potential of a blocking thread.

Where to Go From Here?

You’ve learned so much about the Kotlin Coroutines API and you should be pretty proud of yourself! At this point, you’re ready to migrate from callback-based code to coroutine-powered functions. You should be able to write code that runs multiple functions in parallel, ultimately combining their result in one function call.

Continue reading through this section to learn more about the CoroutineContext, cancellation, exception handling and how to best build a context switching mechanism when moving between multiple threads in your code!

By the time you finish the remaining chapters, you’ll be a master of the Kotlin Coroutines API and you’ll go over all the fundamental concepts that make the API so powerful.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.