Coroutines are components of a computer program that can be considered lightweight threads. Coroutines allow us to suspend the invocation of a function without blocking a thread. Let's imagine a case in which you need to perform a request on the server and display a progress bar until your app receives the response. A request is a long-term operation that should be performed asynchronously because a user interface should stay responsive. This is a common approach to running a new thread that uses a callback so that you're notified when the app receives a response. However, using code with callbacks looks unnatural, complex, and can lead to bugs.
Coroutines can be considered as a library that wraps a particular part of code with the creation of new threads and callbacks. This approach allows you to write asynchronous code in a way that looks as if it were sequentially executed.
The following example demonstrates this:
fun main(args: Array<String>) {
launch {
delay(500L)
println(Thread.currentThread().name)
}
println(Thread.currentThread().name)
Thread.currentThread().join()
}
The output is as follows:
main
ForkJoinPool.commonPool-worker-1
launch is a special function that creates and runs a new coroutine. This function isn't contained in the Kotlin Standard Library, and you need to include the kotlinx-coroutines-core library to use it. If you use the Gradle build tool, you should add the following line:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.0'
This should be added to the dependencies section of your build.gradle file. We will look at the launch function in more detail a little bit later.
To support coroutines, Kotlin only contains the suspend keyword, which can be applied to a function or a lambda. The Kotlin Standard Library also contains base classes, and interfaces describe a coroutine in programming code, such as Continuation and CoroutineContext. The Continuation interface looks as follows:
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resume(value: T)
public fun resumeWithException(exception: Throwable)
}
This interface represents a continuation after a suspension point. It contains the resume and resumeWithException functions, which are used to return the result value or an exception to the outer scope.
Each coroutine runs in a context that is represented by the CoroutineContext interface. A simplified version may look like this:
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
///......
}
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
}
The Element interface looks as follows:
public interface Element : CoroutineContext {
public val key: Key<*>
@Suppress("UNCHECKED_CAST")
public override operator fun <E : Element> get(key: Key<E>): E? =
if (this.key === key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key === key) EmptyCoroutineContext else this
}
The main elements are the instances of the Job and CoroutineDispatcher classes. Under the hood, coroutines use the usual threads, and CoroutineDispatcher decides which thread or threads are used by a coroutine. The launch function returns an instance of the Job class that implements the Element interface and can be used to cancel the execution of a coroutine. To demonstrate this, you can rewrite the preceding example as follows:
fun main(args: Array<String>) {
val job = launch {
delay(500L)
println(Thread.currentThread().name)
}
println(Thread.currentThread().name)
job.cancel()
Thread.currentThread().join()
}
The following is the output:
main
ForkJoinPool.commonPool-worker-1
The Job class also contains the join function, which can be invoked from another suspended function or a coroutine that suspends a callee function. We can use the join function to make the main thread wait until the job is complete. We should rewrite our example as follows:
fun main(args: Array<String>) = runBlocking {
val job = launch {
delay(500L)
println("Coroutine!")
}
println("Hello,")
job.join()
}
The output is as follows:
Hello,
Coroutine!
We can set up a breakpoint on the line, as shown in the following screenshot:
In the debug window, we can see that the main thread waits while a coroutine is running: