Overview
Hi! I am @padobariya working as a mobile engineer with Quipper (Japan office).
In this post, I will talk about basics of Kotlin coroutines, as many of you may already know Kotlin coroutines are no longer experimental as of Kotlin 1.3. It is one of most promising feature for writing asynchronous, non-blocking code (and much more) which can help you to get rid of callbacks hell in your code base. We will go through how to start and some basics of using Kotlin coroutines with the help of the kotlinx.coroutines library, which is a collection of helpers and wrappers for existing Java libraries.
Summary
I guess one of the most challenging things in software development is anything that is asynchronous. Over the time we’ve seen many asynchronous APIs and libraries and Kotlin Coroutines is the latest addition to the toolbox.
One can think of a coroutine as a light-weight thread. Like threads, coroutines can run in parallel, wait for each other and communicate. The biggest difference is that coroutines are very lightweight, almost free: we can create thousands of them, and pay very little in terms of performance.
For me, the best part about coroutines is the structure of the code doesn’t change if you compare it with something synchronous. You don't need to learn any new programming paradigms in order to use coroutines.
Let's get started.
Dependencies
As you may have already know coroutine is not a part of Kotlin core API, we need to add following libraries in order to use Kotlin coroutines (you can also add other modules that you need)
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0-RC1' }
Note: I’ve added kotlinx-coroutines-android dependency which is android specific, you don’t need to add if you are not developing for Android. Basically, it provides Dispatchers.Main to run coroutines on main/UI thread.
Launching a coroutine
You can start new coroutine with the help of launch or async function. Conceptually, async is just like launch difference is as follows. Note: async and await are not keywords in Kotlin and are not even part of its standard library.
Launch
Launches new coroutine without blocking current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled. You can call join on this Job to block until this launch thread completes. Let’s launch our first coroutine using launch
fun foo() { GlobalScope.launch { // launch new coroutine in background delay(1000L) // non-blocking delay for 1 second println("World!") // print after delay } println("Hello,") // main thread continues while coroutine is delayed Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive }
This will generates following output.
Hello, World!
Here we used non-blocking delay(...)
and blocking Thread.sleep(...)
bit confusing right?
We can define this more clear way using runBlocking {...}
fun foo() { GlobalScope.launch { // launch new coroutine in background and continue delay(1000L) println("World!") } println("Hello,") // main thread continues here immediately runBlocking { // but this expression blocks the main thread delay(2000L) // ... while we delay for 2 seconds to keep JVM alive } }
The result is the same, but this code uses only non-blocking delay. The main thread, that invokes runBlocking, blocks until the coroutine inside runBlocking completes.
We can write above in more idiomatic way by wrapping foo()
in runBlocking{...}
fun foo() = runBlocing<Unit> { GlobalScope.launch { // launch new coroutine in background and continue delay(1000L) println("World!") } println("Hello,") // main coroutine continues here immediately delay(2000L) // delaying for 2 seconds to keep JVM alive }
Here runBlocking
Now, as you may have noticed delaying for a time while another coroutine is working is not a good approach. So let's explicitly wait (in a non-blocking way) until the background Job that we have launched is complete:
//With Join fun foo() = runBlocking { // launch new coroutine and keep a reference to its Job val job = GlobalScope.launch { delay(1000L) println("World!") } println("Hello,") job.join() // wait until child coroutine completes }
Now the result is still the same, but the code of the main coroutine is not tied to the duration of the background job in any way. Much better.
Async
Conceptually, async is just like launch. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines. The difference is that launch returns a Job and does not carry any resulting value, while async returns a Deferred -- a light-weight non-blocking future that represents a promise to provide a result later. Basically, async starts a background thread, does something, and returns a token immediately as Deferred.
fun foo() = runBlocking<Unit> { val time = measureTimeMillis { val one = async { doSomethingOne() } val two = async { doSomethingTwo() } println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms") }
You can use .await() on a deferred value to get its eventual result, but Deferred is also a Job, so you can cancel it if needed.
Dispatchers and threads
When we launch coroutine we need to specify dispatchers which determines what thread or threads the corresponding coroutine uses for its execution. If you are an android developer you will use the following departures:
- Dispatchers.Main : Dispatch execution onto the Android main UI thread (Android Specific)
- Dispatchers.IO : Dispatch execution in the background thread
Coroutine scope
As you may have noticed earlier we have used GlobalScope to launch coroutine. Basically, you need to provide scope to each coroutine it can be CoroutineScope or GlobalScope GlobalScope: As the name suggests the lifetime of the new coroutine is limited only by the lifetime of the whole application. CoroutineScope: This is lifecycle aware scope. This should be implemented on entities with a well-defined lifecycle that are responsible for launching children coroutines.
//Global scope Class MainFragment : Fragment() { Private fun loadData() = GlobalScope.launch {} } //Coroutine scope class MainFragment : Fragment(), CoroutineScope { override val coroutineContext: CoroutineContext get() = Dispatchers.Main private fun loadData() = launch {} }
Coroutine using withContext
withContext is used to switch coroutine context. In following snippet loadData() will be executed in background/IO thread.
private fun loadData() = GlobalScope.launch(Dispatches.Main) { view.showLoading() // main thread //background thread val result = withContext(Dispatchers.IO) {provider.loadData()} view.showData(result) // main thread }
The parent coroutine is launched via the launch function with the Main dispatcher. The background job is executed via withContext function with the IO dispatcher.
Multiple tasks sequentially In following case, result1 and result2 are executed sequentially
private fun loadData() = GlobalScope.launch(Dispatches.Main) { view.showLoading() // main thread //background thread val result1 = withContext(Dispatchers.IO) {provider.loadData1()} //background thread val result2 = withContext(Dispatchers.IO) {provider.loadData2()} val result = result1 + result2 view.showData(result) // main thread }
Multiple tasks parallel
In following case result1 and result2 are executed in parallel
private fun loadData() = GlobalScope.launch(Dispatches.Main) { view.showLoading() // main thread //background thread val result1 = async(Dispatchers.IO) {provider.loadData1()} //background thread val result2 = async(Dispatchers.IO) {provider.loadData2()} val result = result1 + result2 view.showData(result) // main thread }
Timeout
We can also specify timeout for a coroutine job.
private fun loadData() = GlobalScope.launch(Dispatches.Main) { view.showLoading() // main thread val task = async(Dispatchers.IO) {provider.loadData()} //background thread val result = withTimeoutOrNull(5, TimeUnit.SECOND) {task.await()} view.showData(result) // main thread }
Exception Handling
So far we concluded that coroutines are awesome! but sometimes the world is not as awesome as we think and we have to deal with it. In coroutines same as synchronous programming you can use try catch to handle exceptions.
As the post is already getting long we can not cover all aspects of exception handling here. I strongly suggest viewing KotlinConf 2018 video to know how exception handling works in complex coroutines system.
private fun loadData() = GlobalScope.launch(Dispatches.Main) { view.showLoading() // main thread try { //background thread val result = async(Dispatchers.IO) {provider.loadData()} view.showData(result) // main thread } catch(ex: Exception) { ex.printStackTrace() } }
To avoid try catch in parent class you can catch exception in loadData() and return generic result.
To Be Continued …
Thank you for giving this article a read. I hope you found this article interesting. In the next article, we will dive into more details about coroutines performance and how we can replace RxJava with coroutines.
Ohh by the way, here are some helpful links to help you with understanding coroutines