Here's how we used AWS services for long-running tasks.
Understanding Kotlin Coroutines with examples
There’s a wide range of asynchronous programming options available to us on the JVM platform.
Some of these include the well-known RxJava/RxKotlin library for Java and Kotlin, as well as the outdated callbacks approach.
The preferred method for managing async tasks in modern app development, especially on the Android platform where Kotlin is the official language for development, is by using coroutines.
Here we’ll explore Kotlin coroutines, and why they’re better than alternatives.
Table of Contents
Asynchronous programming
Let’s first examine what asynchronous programming actually is and why it is so important in UI frameworks like Android before we begin to examine all the available options.
When we call an asynchronous function that returns the result at a later time rather than immediately, we’re dealing with asynchronous programming.
The most typical cases in Android involve performing a network request or querying a database for some data.
Need an Android app?
We have a dedicated team just for you →
You’ll be talking with our technology experts.
Now you’re probably wondering why we don’t just wait for the network or database response using a synchronous function. Unfortunately, that isn’t possible because the majority of the code we write runs on the main (UI) thread.
This thread performs crucial tasks like drawing the user interface (UI) on the screen and monitoring user interaction.
Therefore, the UI would freeze and user interaction with the application wouldn’t be possible if the main thread became blocked while waiting for the response.
Because of that a background thread is waiting for the response and alerts the main thread only when it is ready to provide a result, this is why we use asynchronous methods.
How to achieve async tasks
Now that we understand what asynchronous programming is all about, let’s explore the possibilities we could use to achieve it.
Callbacks
As already mentioned, one of the possibilities is using callbacks.
The idea behind callbacks is to write functions that are non-blocking and pass them a function that should be executed once the process started by the callback function has finished.
Using callback, functions can create a lot of problems such as:
- supporting cancelation requires a lot of additional effort
- code with multiple callbacks is often unreadable
- hard to achieve parallelized tasks
That’s why using a callback is not recommended in dealing with complex cases.
RxJava
The other mentioned approach is using reactive streams.
Using this approach, all operations happen inside a stream of data that can be started, processed, and observed.
This is definitely a better solution than using callbacks: cancelation is supported, there are no memory leaks, and proper use of threads is enabled.
By using reactive streams, we achieve truly concurrent and memory leaks free code, but it’s quite complicated.
There is a need to learn all these functions, such as subscribeOn, observeOn, and subscribe.
Functions need to return objects wrapped inside Single or Observable classes. Briefly, the code gets complicated.
What are Kotlin coroutines?
Coroutines are a Kotlin library for handling async tasks. They make asynchronous code easier to write and read.
The functionality that coroutines provide is the ability to suspend a coroutine at some point and resume it in the future. Thanks to that functionality, we can run our code on the Main thread and suspend it when we request data from an API.
When a coroutine is suspended, the thread is not blocked (this is called “suspension” and will be explained below) and is free to be used for other functionality for example, showing a progress bar indicator.
Once the data is retrieved from the API, the coroutine waits for the Main thread. When it gets the thread, it will continue from the point where it stopped.
Here are some of the reasons you should pick coroutines over callbacks and RxJava:
lightweight – thanks to the suspending feature, coroutines don’t block the thread so multiple asynchronous coroutines can run on the same thread
cancelation support – it’s easy to cancel a running coroutine.
efficient memory management – by canceling the coroutine scope, all running coroutines inside the scope get canceled.
easy Jetpack integration – lots of Jetpack libraries support coroutine integration (WorkManager, ViewModel, Room…).
Now let’s talk about the main feature of Coroutines.
Suspension
The suspension capability is the most essential feature upon which all Kotlin coroutines are built.
Suspending a coroutine means stopping it in the middle of a process and continuing it at a later time in the future.
When it’s suspended, it returns Continuation, which allows it to continue where it was suspended. For example, it’s just like a checkpoint in a game, which allows us to continue from the point we have stopped.
Using the Continuation interface, it is possible to resume the coroutine with or without a return value or with an exception if an error occurred while it was suspended.
Now that we understand how suspension works, let’s look at some examples of how to resume a coroutine.
For this, we need a coroutine. Coroutines are started using coroutine builders which we will explore later. For now, we can just use the suspending main function.
Resume example:
suspend fun main(){ println("Start") delay(1000) suspendCoroutine { continuation -> println("Resumed after 1 second") continuation.resume(Unit) } println("End") }
The first thing to note in this example is the crucial word suspend, which stands for a function that suspends.
Functions that can suspend a coroutine are known as suspending functions. They must be called from a coroutine or another suspending function.
At the beginning of the function, we print a string “Start,” which represents the start of the function.
In the next line, we use the delay function, which “pauses” the coroutine for 1 second.
Now take a look at the suspendCoroutine invocation and notice that it ends with a lambda expression.
The function passed as an argument will be invoked before the suspension because, after the suspendCoroutine call, it would be too late. Now we can easily resume the coroutine using the stored continuation inside the lambda expression, and the other two strings will be printed after 1 second.
If we didn’t use continuation.resume( ), we would be stuck at that line, and “End” would never be printed.
Resume with value example:
suspend fun main(){ val number: Int = suspendCoroutine<Int> {number -> number.resume(7) } println(number) }
In the example above, when we resume a coroutine without a value, we pass Unit to the resume function, but why? Because Unit is the generic type of the Continuation parameter and it is also returned from the function, a non-meaningful value is actually returned in that case.
On the other hand, we have specified which type (Int) will be returned in its continuation, and the same type needs to be used when we call the resume function. It is possible to return whatever object you want: Boolean, String, or some custom object.
Resume with an exception example:
suspend fun main() { try { suspendCoroutine { continuation -> continuation.resumeWithException(Throwable("An error accrued.")) } } catch (e: Throwable) { println(e) } }
As every function that is called can return a value, it can also throw an exception; the same goes for the suspendCoroutine function.
In the examples above, we can see that when a resume is called, it returns data passed as an argument. The same goes for resumeWithException. When it is called, the exception passed as an argument is thrown.
Now that we know the basic functionality of what suspension is and how it works, let’s explore some additional coroutine features and see how they are actually created and used.
Kotlinx coroutines library
The first section covered the basics of suspending functions, what suspension actually is, and how it works.
This simple functionality is achievable by using built-in support in the Kotlin programming language.
For more advanced features, we need to use the external kotlinx.coroutines library, which we’re going to explore now.
Coroutine builders
We already mentioned that coroutines are started using coroutine builders.
So, what exactly are coroutine builders? Let’s say that they are a connection between the suspending and non-suspending parts of our code. Since they are not suspended, they can be easily called from other non-suspending pieces of code. So let’s explore the three most essential coroutine builders.
launch
Using launch we start a new coroutine without blocking the current thread which can be seen in the example below.
launch example:
fun main() { println("Start") GlobalScope.launch { delay(3000) println("Hello World!") } println("End") Thread.sleep(5000L) }
The launch function is an extension function on the CoroutineScope interface.
For learning practices, it’s fine to use the GlobalScope, but in standard practice, we should avoid using it because the coroutine would be running as long as the application is running, and we would waste a lot of resources.
Another thing to notice here is that we need to call Thread.sleep at the end of the function. Without this line of code, the coroutine would end immediately after it was launched, and “Hello World!” wouldn’t be printed. This is because the delay function doesn’t block the thread; it suspends it.
runBlocking
The core functionality of coroutines is that they don’t block threads, only suspend them. There are cases, however, where blocking the thread can be useful. In the launch builder example, we needed to use the Thread.sleep function so the program wouldn’t end too soon.
Now let’s see how we can achieve the same behavior using runBlocking.
runBlocking example:
fun main() { println("Start") runBlocking { delay(3000) println("Hello World!") } println("End") }
async
The async builder works very similarly to launch, but it produces a value. It’s often used to perform asynchronous tasks, such as obtaining data from two different places. It returns an object of type Deferred<T>, where T is the type of the produced data.
async example:
suspend fun getLongitude(): String { delay(1500) return "110.366457" } suspend fun getLatitude(): String { delay(1500) return "37.463055" } data class Location( val longitude: String, val latitude: String ) fun main() = runBlocking { val longitude = GlobalScope.async { getLongitude() } val latitude = GlobalScope.async { getLatitude() } val location = Location(longitude.await(), latitude.await()) println("Users current location is: $location") }
Here we can see a much more practical example of the async builder. We are simulating a fetch of the user’s current location from an API.
Again, we used the GlobalScope to call the async builder, which is not ideal but adequate for learning.
Then we parallelized two tasks instead of calling them one after another.
After that, we created the location object, which will hold the retrieved data, using the await function.
The returned data is stored inside Deferred, and once it is ready to be used, it will be returned from the await function and the location object will be created.
We used runBlocking once more to keep the program running until the fetching process was completed.
Coroutine context
The coroutine context is part of the core of every coroutine. Every coroutine we start is tied to a specific coroutine context. The context is defined inside the scope in which we start our coroutines.
CoroutineContext is defined as an interface.
If you open the documentation of the interface you can see that the context is defined as an indexed set of Element instances, so the coroutine context consists of several context elements.
The most important elements are: CoroutineDispatcher, Job, CoroutineName and the ErrorHandler which we will explore below.
Job
A job represents a cancelable thing with a lifecycle. The lifecycle is represented by its state. Here on the graph, you can see the states and the transitions between them.
When the job is in the “active” state, it’s performing its job. In this state, child coroutines can be started. Most coroutines will start in the “active” state; only those that are started lazily will start in the “new” state.
When a coroutine is executing its body, it’s in the “active” state.
After it’s done, its state changes to “completing,” where it waits for its children. After all its children are done, the state of the job changes to “completed.”
On the other hand, if a job cancels while running, its state changes to “canceling,” and after that to the “canceled” state.
Here you can see some examples that represent jobs in different states.
The first example shows how to create a job using a factory function Job( ). By default it is in the “active” state until we call job.complete( ) to complete it, and now the state changes to “completed”.
active job example:
suspend fun main() = coroutineScope { val job = Job() println(job) job.complete() println(job) }
The second example shows a job launch that started lazily. As mentioned above, lazily started jobs are in the “new” state. Using the start( ) function, we change its state to “active.”
And lastly, we use join( ) to await coroutine completion.
lazily started job example:
suspend fun main() = coroutineScope { val lazyJob = launch(start = CoroutineStart.LAZY) { delay(2500) } println(lazyJob) lazyJob.start() println(lazyJob) lazyJob.join() println(lazyJob) }
Another thing to notice here is that we used the coroutineScope function. It’s a standard function we use when we need a scope inside a coroutine function. Don’t worry about that for now; I’ll explain it in the Coroutine scope section.
Cancelation
After finishing some work, we should always clean our workspace. Put the tools away, clean off the dust, and so on.
The same rule applies to using coroutines. We need to make sure that we control the life cycle of the coroutine and cancel it when it’s no longer in use, because it can waste resources.
The previously mentioned Job interface has a cancel function, which allows its cancellation. When a job is canceled, its state changes to “canceling,” and at the first suspension point, a CancelationException is thrown.
cancelation example:
suspend fun main() = coroutineScope { val job = Job() launch(job) { for (number in 1..10) { delay(200) println("Current number: $number") } } delay(2500) job.cancelAndJoin() if (job.isCancelled){ println("Job canceled.") } else { println("Job is still running...") } }
Once a Job is canceled it can’t be used as a parent for any new coroutines.
Here we can see a simple example of how it’s possible to cancel a job.
First, we create a job instance using the Job( ) factory function. We pass it as the coroutineContext element to the launch builder in which we simulate a process of printing numbers that lasts for 2000 milliseconds.
Below the launch builder we delay the job cancelation for 2500 milliseconds because we want to print all the numbers and only then cancel the job.
If we used a shorter time interval for the delay of canceling the job, it would be canceled before all the numbers were printed. For the cancelation, we use the cancelAndJoin( ) function.
It’s practical because it combines two functionalities at once, the cancel function and the join function which we use to wait for the cancelation to finish before we can proceed.
In the end, we simply check if the job was successfully canceled using the isCanceled function.
For more information about cancellation, you can check the official Kotlin guides.
Exception handling
Errors, or rather accidents, happen every day in our lives, and the same happens in apps too. Even the most tested systems have bugs and unwanted behaviors.
If an uncaught exception slips by the program brakes, the same happens to the coroutine. So it is very important to have a good exception handling mechanism to catch them all.
The two most popular ways to handle coroutine exceptions are by using try-catch and the CoroutineExceptionHandler.
try-catch example:
fun main(): Unit = runBlocking { val scope = CoroutineScope(Job()) scope.launch { try { throw RuntimeException() } catch (exc: Exception) { println("Caught: $exc") } } }
Here is a simple example of using the try-catch block to catch an exception.
If a coroutine doesn’t catch an exception, it doesn’t really throw the exception in the traditional way as, for instance, a regular function would.
Instead, it uses another exception-handling mechanism for propagating the exception up the hierarchy. Here we won’t go deep into structured concurrency and job hierarchy because it would be too much for this article, but you can check out more details about it here.
However, how can uncaught exceptions from failing coroutines be handled at the top of the hierarchy?
Well, we can handle them by using a CoroutineExceptionHandler. In the example below, the exceptionHandler is defined as a read-only variable that gets the coroutine context of the failed coroutine and the propagated exception as the input parameters.
After the creation of the exceptionHandler it can be passed as a context element to the launch builder.
CoroutineExceptionHandler example:
fun main(): Unit = runBlocking { val exceptionHandler = CoroutineExceptionHandler { ctx, exception -> println("Caught $exception using CoroutineExceptionHandler") } val scope = CoroutineScope(Job() + exceptionHandler) scope.launch { throw RuntimeException() } delay(1000) }
Coroutine scope
All coroutines run within a specific scope, which allows them to be managed as groups instead of individual coroutines.
This is very helpful on Android when we need to cancel coroutines. For example, when a fragment or activity gets destroyed, it needs to be ensured that coroutines don’t leak, or in other words, continue running in the background.
By assigning coroutines to a scope, for example, all can be canceled together when they are no longer needed.
Using the CoroutineScope interface, there is an option to create a custom scope, but there are already some built-in scopes:
GlobalScope – it’s used to launch top-level coroutines, which are tied to the entire lifecycle of the application. As we already mentioned, the use of this scope is not recommended, especially in Android applications, since it has the potential for coroutines to continue running when they are no longer needed.
Learn from a software company founder.
Make your software product successful with monthly insights from our own Marko Strizic.
ViewModelScope – provided specifically when using the Jetpack architecture ViewModel component. Coroutines launched in this scope within a ViewModel are automatically canceled by the runtime system when the corresponding ViewModel instance gets destroyed.
We already covered the three main coroutine builders but that’s not all there are.
In previous code examples to launch a coroutine, we used a coroutineScope builder.
This builder is ideal for situations where a suspend function launches multiple coroutines that run in parallel and where some action needs to be performed only when all the coroutines are finished.
Coroutine dispatchers
The dispatcher is responsible for assigning coroutines to appropriate threads (or pool of threads) and suspending and resuming the coroutine during its lifecycle.
Dispatchers in Kotlin Coroutines are a similar concept to Schedulers in RxJava.
Default dispatcher
If there is no specific definition which dispatcher to use, the one chosen by default is Dispatchers.Default, which is designed to run CPU intensive operations such as sorting data or performing complex calculations.
code example:
suspend fun main() = coroutineScope { val logger = KotlinLogging.logger {} repeat(100) { launch(Dispatchers.Default) { while (true) { logger.info { "Some busy work here..." } } } } } }
In this example, we simulate some CPU-intensive work to show how to use the default dispatcher.
We pass it as a context element to the launch builder. Here, if we didn’t pass Dispatcher.Default as the desired dispatcher, it would be set as the default one anyway.
And in the coroutine we just log some random string. If you start this code and look at the log data, you can see something like this:
[DefaultDispatcher-worker-8] INFO Main – Some busy work here…
The first part of the string represents the current thread name on which the task is running. I have a PC with a CPU that contains 8 cores, so there are 8 threads in the pool.
IO dispatcher
This dispatcher is intended to be used when the threads are blocked with IO operations such as performing network, disk, or database operations.
Main dispatcher
This dispatcher runs the coroutine on the main (which is generally the most important thread) thread and it is suitable for coroutines that need to make changes to the UI.
On Android, it is the only one that can be used for user interaction with the UI. Therefore, it needs to be used very carefully. If the main thread is blocked, the whole application is frozen. To run a coroutine on the main thread, we use Dispatchers.Main.
Conclusion
Coroutines provide a simpler and more efficient approach to performing asynchronous tasks than that offered by traditional multi-threading.
They allow asynchronous tasks to be implemented in a structured way without the need to implement callbacks or by using libraries such as RxJava.
Some parts, in the beginning, are hard to understand.
I know I was there too. But I hope that won’t discourage you and you will continue to explore more and dive deeper in the asynchronous world of coroutines.
If you need help with software development, you can always talk to our experts.
Till next time, happy coding. 😊✌🏼