Chapters

Hide chapters

Kotlin Apprentice

Third Edition · Android 11 · Kotlin 1.4 · IntelliJ IDEA 2020.3

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

23. Kotlin Coroutines
Written by Irina Galata

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

While working through the previous chapters, you’ve run synchronous code only. That means that one command was executed after another by your CPU, sequentially, and no code in your projects were running simultaneously on different computing cores (in the case that your CPU has them, which they tend to these days).

Consequently, if you decided to perform any long-running, time-consuming operations (e.g., sending a request over a network to a server, or processing a large file), your program would appear to freeze until the operation finished, and a user would have to wait. That’s less than ideal — a user should be able to interact with your program even while it’s executing a difficult task. That expectation leads to the concept of the asynchronous programming.

Asynchronous programming

As opposed to the synchronous approach, asynchronous programming allows for the execution of several tasks in parallel at the same time. That way, you can render a beautiful loader animation while your app is also retrieving the necessary data from a server, for example. Or you could break up a non-trivial task into a few easier ones and execute them simultaneously to decrease the processing time.

Threads

In Java — and accordingly in Kotlin on the JVM — you can parallelize your program using threads. Each java.lang.Thread object represents one execution flow, which sequentially performs the commands within the single thread.

fun main() {
  thread(start = true, name = "another thread") {
    (0..10).forEach {
      println(
        "Message #$it from the ${Thread.currentThread().name}")
    }
  }

  (0..10).forEach {
    println(
      "Message #$it from the ${Thread.currentThread().name}")
  }
}

Coroutines

There isn’t an immediate better option in the Java language but, in Kotlin, you receive coroutines right out of the box! A coroutine is primarily a computation. Its defining feature is that it can be suspended and resumed at specified points of the computation without blocking a thread. Suspension is an extremely efficient operation. You can create hundreds and even thousands of coroutines and run them concurrently, as they are lightweight and don’t require many extra resources for their execution.

Getting started

Open up the starter project for this chapter. The starter project contains a non-coroutine version of the example project you’ll build below using coroutines. The main() function in main.kt looks as follows:

fun main() {
  BuildingYard.startProject("Smart house", 20)
}
dependencies {
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0")
}
fun main() = runBlocking {
  launch(Dispatchers.Default) {
    (0..10).forEach {
      println("Message #$it from the ${Thread.currentThread().name}")
    }
  }

  (0..10).forEach {
    println("Message #$it from the ${Thread.currentThread().name}")
  }
}
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

Configuring coroutines

Kotlin coroutines are an extremely flexible solution for the wide variety of cases you may have. And the way a coroutine behaves is pretty much defined by its context. Any coroutine gets executed inside some CoroutineScope containing an instance of CoroutineContext, which is represented by a collection containing important configurations. You’re going to get acquainted with the most important of them - Job, Dispatcher and, later in this chapter, CoroutineExceptionHandler.

Job

Job basically represents a background job, which has a state (active, cancelled, completed, etc.), optionally has children, and can be started and cancelled. You’ll learn more about Job in this chapter.

Dispatchers

Dispatchers are responsible for the threads where your coroutines are executed. There are some ready-to-use dispatchers in the Kotlin core library:

CoroutineScope

CoroutineScope is an interface which does nothing except provide an associated CoroutineContext:

public interface CoroutineScope {
  public val coroutineContext: CoroutineContext
}

Obtaining a scope

There are multiple ways to get CoroutineScope to launch a coroutine. Some of them are mentioned here:

Coroutines builders

In order to use coroutines and therefore parallelize the execution of your code, you need to use coroutine builders. They’re regular functions that create a new coroutine inside a specified CoroutineContext. You’ve already seen some of them in the code snippet above — runBlocking() and launch(). Let’s find out how they work.

runBlocking()

The declaration of the runBlocking() function in the coroutine library code is as follows:

public fun <T> runBlocking(
  context: CoroutineContext = EmptyCoroutineContext,
  block: suspend CoroutineScope.() -> T
): T

launch()

The example code also used the launch() function, which has the following signature:

public fun CoroutineScope.launch(
  context: CoroutineContext = EmptyCoroutineContext,
  start: CoroutineStart = CoroutineStart.DEFAULT,
  block: suspend CoroutineScope.() -> Unit
): Job
public suspend fun join()
launch { postVideoToFeed() }.join()

CoroutineStart

As you can see from the launch function declaration, you can specify not only the threads on which your coroutine will be launched, but also the moment when it should happen. There are four options:

async()

There will be numerous cases where you are interested not only in waiting for the coroutine to be executed, but also in getting a result from it. The most common case is getting data from a server—for example, loading a user profile or getting a list of chat messages. async() is a definite solution for this case:

public fun <T> CoroutineScope.async(
  context: CoroutineContext = EmptyCoroutineContext,
  start: CoroutineStart = CoroutineStart.DEFAULT,
  block: suspend CoroutineScope.() -> T
): Deferred<T>
public suspend fun await(): T
val userData = async { getUserDataFromServer() }.await()

withContext()

The withContext() function gets the result of the execution as well. However, it’s optimized for more straightforward cases, when you don’t need the Deferred instance but just the result itself:

public suspend fun <T> withContext(
  context: CoroutineContext,
  block: suspend CoroutineScope.() -> T
): T

Example: A high-rise building

To illustrate all the niceties of coroutines, it’s necessary to imagine a process or task, some parts of which could be executed simultaneously, while other parts should be completed strictly one after another. The process of constructing a high-rise building is a good example.

class Building(val name: String) {

  fun makeFoundation() {
    Thread.sleep(300)
    speakThroughBullhorn("The foundation is ready")
  }

  fun buildFloor(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("The $floor'th floor is raised")
  }

  fun placeWindows(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("Windows are placed on the $floor'th floor")
  }

  fun installDoors(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("Doors are installed on the $floor'th floor")
  }

  fun provideElectricity(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("Electricity is provided on the $floor'th floor")
  }

  fun buildRoof() {
    Thread.sleep(200)
    speakThroughBullhorn("The roof is ready")
  }

  fun fitOut(floor: Int) {
    Thread.sleep(200)
    speakThroughBullhorn("The $floor'th floor is furnished")  
  }

  fun speakThroughBullhorn(message: String) = println(message)

}
class Building(
  val name: String,
  var floors: Int = 0,
  private val scope: CoroutineScope
) {

  suspend fun makeFoundation() = scope.launch {
    delay(300)
    speakThroughBullhorn("[${Thread.currentThread().name}] The foundation is ready")
  }

  suspend fun buildFloor(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Floor number $floor floor is built")
    ++floors
  }

  suspend fun placeWindows(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Windows are placed on floor number $floor")
  }

  suspend fun installDoors(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Doors are installed on floor number $floor")
  }

  suspend fun provideElectricity(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Electricity is provided on floor number $floor")
  }

  suspend fun buildRoof() = scope.launch {
    delay(200)
    speakThroughBullhorn("[${Thread.currentThread().name}] The roof is ready")
  }

  suspend fun fitOut(floor: Int) = scope.launch {
    delay(200)
    speakThroughBullhorn("[${Thread.currentThread().name}] Floor number $floor is furnished")
  }

  fun speakThroughBullhorn(message: String) = println(message)
}
class BuildingYard {
  suspend fun startProject(name: String, floors: Int) {

  }
}
fun main() = runBlocking {
  BuildingYard().startProject("Smart house", 20)
}
suspend fun startProject(name: String, floors: Int) {
  val building = withContext(Dispatchers.Default) {
    val building = Building(name, scope = this)
    val cores = Runtime.getRuntime().availableProcessors()
    building.speakThroughBullhorn(
      "The building of $name is started with $cores building machines engaged")
    building.makeFoundation().join()
    building
  }
  if (building.floors == floors) {
    building.speakThroughBullhorn("${building.name} is ready!")
  }
}

(1..floors).forEach {
  // A floor should be raised before we can decorate it
  building.buildFloor(it).join()

  // These decorations could be made at the same time
  building.placeWindows(it)
  building.installDoors(it)
  building.provideElectricity(it)
  building.fitOut(it)
}

building.buildRoof().join()

Error handling

The common approach to handle exceptions while using coroutines is a well-known try-catch block. The way you catch exceptions in synchronous code is still applicable here:

try {
  val userProfile = scope.withContext(Dispatchers.IO) {
    getProfile()
  }
} catch (e: NoSuchUserException) {
  // handle exception
}

Using CoroutineExceptionHandler

There could be a case when you need to have a global exception handler for all your coroutines, and CoroutineExceptionHandler is designed for this purpose:

val scope = CoroutineScope(Dispatchers.Default)
val handler = CoroutineExceptionHandler { context, exception ->
  println(exception.message)
}
scope.launch(handler) {
  uploadData()
}

Understanding coroutines

Coroutines aren’t a new concept in software development; several programming languages — such as C#, Ruby and Python — have supported them for a long time. In many languages, coroutines are based on state machines, and Kotlin isn’t an exception.

Challenges

Challenge 1

Modify the BuildingYard class in such way that you could build several buildings simultaneously, not one by one. (Hint: Consider using Collection<Deferred<T>>.awaitAll())

Challenge 2

Modify the Building class in such way so the buildFloor() function could fail randomly (i.e., throw an exception). In the BuildingYard class, after this function execution completes, check whether it executed successfully. If it is unsuccessful, start the execution of the task again.

Key points

Where to go from here?

To continue your coroutines learning journey you may find it exciting to get to know some more advanced, topics, like Channels and Flows. The official documentation would be a great departing point! Those will help you to make use of streams of data, not just a single value. You can learn about Channels at https://kotlinlang.org/docs/reference/coroutines/channels.html and Flows at https://kotlinlang.org/docs/reference/coroutines/flow.html.

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.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now