Introduction to Kotlin Coroutines for Beginners

Vermasanchit
BYJU’S Exam Prep Engineering
9 min readJun 1, 2021

--

Introduction

For almost all modern applications, Asynchronous Programming is very important. It allows performing heavy-duty tasks away from the UI thread, in the background paralelly. By doing this, you can avoid UI freezes, and make the user experience fluid.

Android provides several asynchronous programming tools like RxJava, AsyncTasks, Jobs, Threads but it’s difficult to find the most appropriate one to use. We basically started handling this using the callback mechanism. We do something like hitting an API and wait for the callback to get invoked where we process the result. But this can introduce a ton of code. Not only that, but the code can become unreadable, as you introduce more callbacks.

If you’ve worked with Rx, exploring its list of operators in-depth while performing complicated operations and apply them correctly. On the other hand, AsyncTasks and Threads can easily introduce leaks and memory overhead.

Now we have another tool to write asynchronous code more naturally, which is more humanly understandable and readable: Kotlin Coroutines!

What are Coroutines?

Coroutines = Co + Routines

Here, Co means cooperation and Routines means functions.
According to documentation coroutines are nothing but lightweight threads.
The thing to remember is :

Coroutines do not replace threads, it’s more like a framework to manage it.

A framework to manage concurrency in a more performant and simple way with its lightweight thread which is written on top of the actual threading framework to get the most out of it by taking the advantage of cooperative nature of functions.

There is a number of important differences that make their real-life usage very different from threads, like :

  • they are executed inside of a thread, so you can start many coroutines inside a single thread
  • coroutines are suspendable which means we can execute some instructions, pause the coroutine in the middle of execution and let it continue when we wanted to.
  • they can easily change the context which means that a coroutine that was started in one thread can easily switch to another thread

Before diving in further, let’s look at some basic terminology for the Kotlin Coroutines:

  • Suspending functions: This kind of function can be suspended without blocking the current thread. Instead of returning a simple value, it also knows in which context the caller suspended it. Using this, it can resume appropriately, when ready.
  • CoroutineBuilders: These are functions that help us create a coroutine. They can be called from normal functions because they do not suspend themselves. There are a bunch of coroutine builders provided by Kotlin Coroutines, including async(), launch(), runBlocking.
  • CoroutineScope: Helps to define the lifecycle of Kotlin Coroutines. It can be application-wide or bound to a component like the Android Activity. You have to use a scope to start a coroutine.
  • CoroutineDispatcher: Defines thread pools to launch your Kotlin Coroutines in. There are majorly 4 types of Dispatchers: Main, IO, Default, Unconfined.

Suspend Functions

The suspend functions are not any special kind of functions they are just normal functions appended with the suspend modifier to have the superpower of suspending. It allows coroutines to be lightweight and fast because they don’t really cause any overhead, such as threads. Instead, they use predefined resources and smart resource management.

suspend fun doNetworkCall(){//write your code here}

These kinds of functions can be suspended without blocking the current thread and later can be resumed. Like in the image given below FunctionA is suspended while FunctionB continues the execution in the same thread.

The most important thing about suspend functions is that they can only be executed within another suspend function or a coroutine.

GlobalScope.launch{
doSomething()
}
//////OR//////
suspend fun showUsersList(){ doSomething() }suspend fun doSomething(){
//your code
}

Coroutines Builders

Coroutine builders are simple functions that can create a coroutine around coroutine scopes. launch, async, runBlocking are some of the builders given by the library.

  1. launch: it starts a new coroutine that is “fire and forget” — that means it won’t return the result to the caller.
  2. async: it returns a special kind of result called Deferred which can be cancelled or its result can be awaited using await().
  3. runBlocking: it is used to run tasks by blocking whichever thread it’s called on until the block completes.
val job = GlobalScope.launch { 
// fire and forget task is done here
}
____________________________________________________________________
GlobalScope.launch(Dispatchers.Main) {val deferredJob1 = async{
// a job which returns some result
function1()
}
val deferredJob2 = async{
// a job which returns some result
function2()
}
// here function1() and function2() will execute parallelly
// block until deferredJob returns with the result
val result = deferredJob1.await()
}
____________________________________________________________________
runBlocking {// block the calling thread until this block execution isn't
complete
}

Switching Context

Sometimes while doing a task, we may need to do some IO operation, then some DB operation or some kind of computation, and then end with showing some UI on Main thread. This requires us to jump between correct dispatchers.

To do this coroutines provide us withContext() operator. Using this we can switch to a different dispatcher and then come back to the old dispatcher as its block ends.
Remember, calling withContext will suspend the calling function until the withContext block doesn’t end.

val job = GlobalScope.launch(Dispachers.IO) { 
doNetworkCall()
withContext(Dispatcher.Main){
//update ui here
}
}

Coroutine Scope

Scope in which a coroutine runs (similar to variable scope). When the scope ends, so does the coroutine. When a child coroutine is started inside a parent one it inherits parent scope (Unless specified otherwise). It means that when the parent coroutine is stopped, so will the child coroutine since its scope was that of the parent.

There are mainly 3 types of coroutine scope:

  • Global Scope: GlobalScope is alive as long as your app is alive, if you doing some counting for instance in this scope and rotate your device it will continue the task/process.
GlobalScope.launch{
fetchData()
}

Since it is alive as the application is alive, it may produce memory leaks. For example, if there are 2 activities Activity1 and Activity2, you move to Activity2 and make a network call in a GlobalScope. But before completing the network call you moved back to Activity1. Now, since the GlobalScope is alive throughout the application, it will not cancel the call and produce a memory leak.

  • ViewModel Scope: A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically cancelled if the ViewModel is cleared. Under the hood, it uses a CoroutineContext with SupervisorJob and Dispatchers.Main. When the ViewModel is cleared, it executes the clear method before calling onCleared. In the clear method, the ViewModel cancels the Job of the viewModelScope.
viewModelScope.launch{
fetchData()
}
  • LifeCycle Scope: A LifecycleScope is defined for each Lifecycle object. Any coroutine launched in this scope is cancelled when the Lifecycle is destroyed.
lifecycleScope.launch{
fetchData()
}
  • CoroutineScope — This allows you to define a custom scope by providing your own context.
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch{
fetchData()
}

Coroutine Dispatcher

Dispatchers determine what thread or thread pool the coroutine uses for execution. The dispatcher can confine a coroutine to a specific thread. It can also dispatch it to a thread pool. We have mainly 3 types of dispatcher — Main (thread), IO (thread pool), Default (thread pool).

  • Dispatchers.Default: CPU-intensive work, such as sorting large lists, doing complex calculations, and similar. A shared pool of threads on the JVM backs it.
  • Dispatchers.IO: networking or reading and writing from files. In short — any input and output, as the name states
  • Dispatchers.Main: recommended dispatcher for performing UI-related events. For example, showing lists in a RecyclerView, updating Views and so on.
GlobalScope.launch(Dispatchers.Default){
fetchData()
}
GlobalScope.launch(Dispatchers.IO){
fetchData()
}
GlobalScope.launch(Dispatchers.Main){
fetchData()
}

There is another type of dispatcher

Dispatchers.Unconfined: This dispatcher doesn’t confine coroutines to any specific thread. The coroutine starts the execution in the inherited CoroutineDispatcher that called it. But after a suspension ends, it may resume in any other thread.

Note that it’s an advanced mechanism that can be helpful in certain corner cases but, as stated in the official docs, it should not be used in general code.

Coroutine Job

To better manage a Coroutine, a job is provided when we launch (or async etc). When you launch a coroutine, you basically ask the system to execute the code you pass in, using a lambda expression. That code is not executed immediately, but it is, instead, inserted into a queue. A Job is basically a handle to the coroutine in the queue. It only has a few fields and functions, but it provides a lot of extensibility. Some functions out of many that job interface offers are as follows:

val job = GlobalScope.launch(Dispatchers.Default) {val job1 = launch{networkCall1()}
val job2 = launch{networkCall2()}
}
  • join() function is a suspending function, i.e. it can be called from a coroutine or from within another suspending function. Job blocks all the threads until the coroutine in which it is written or has context finished its work. Only when the coroutine gets finishes, lines after the join() function will get executed.
  • cancel() method is used to cancel the coroutine, without waiting for it to finish its work.

A job can go through the states: New, Active, Completing, Completed, Cancelling, and Cancelled. We don’t have access to the states, but we can access the following properties: isActive, isCancelled and isCompleted.

Jobs can be hierarchical (parent-child relationship)

If a launch is triggered in another coroutine (under the same scope context), the job of the launch will be made the child job of the coroutine.

Such a parent-child relationship will trigger some behaviours given below:

  1. if parent job cancel, children’s jobs are cancelled as well
  2. if we cancel the child’s job, the parent’s job continues on.
  3. when a child’s job throw error, the parent’s job is cancelled as well
  4. when the parent’s job error out, the children’s job is cancelled

This can be easily understood with the help of a diagram given below

SupervisorJob:
Children of a supervisor job can fail independently of each other. A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children; so a supervisor can implement a custom policy for handling failures of its children.

Exception Handling

Exception handling in Kotlin Coroutines behaves differently depending on the CoroutineBuilder you are using. The exception may get propagated automatically or it may get deferred till the consumer consumes the result.

Here’s how exceptions behave for the builders you used in your code and how to handle them:

  • launch: The exception propagates to the parent and will fail your coroutine parent-child hierarchy. This will throw an exception in the coroutine thread immediately. You can avoid these exceptions with try/catch blocks or a custom exception handler.
CoroutineScope.launch(Dispatchers.Main) {
try {
doSomething()
}catch (exception: Exception) {
Log.d(TAG, “$exception handled !”)
}
}
///////OR////////val exceptionHandler = CoroutineExceptionHandler {
coroutineContext, exception ->
// handle exception
}
val topLevelScope = CoroutineScope(Job() + exceptionHandler)topLevelScope.launch(exceptionHandler) { … }
  • async: You defer exceptions until you consume the result for the async block. That means if you forgot or did not consume the result of the async block, through await(), you may not get an exception at all! The coroutine will bury it, and your app will be fine. If you want to avoid exceptions from await(), use a try/catch block either on the await() call or within async().
val deferredJob = GlobalScope.async {
fetchData()
}
try {
val data = deferredUser.await()
}catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}

Few extra points about Coroutines

  • withTimeOut(): is designed to cancel the ongoing operation on timeout, which is only possible if the operation in question is cancellable.
  • delay(): delays coroutine for a given time without blocking a thread and resumes it after a specified time.
  • We can make out own dispatcher by wrapping a single thread or thread pool
  • Dispatchers.IO is limited to 64 threads or the number of cores by default.

I hope this article helped you to get some knowledge about Kotlin Coroutines and how to use coroutines in your project.

Please let me know your suggestions and comments.

Thanks for reading :-)

--

--