Kotlin Coroutines Explained for Android

Kavita Agrawal
MobilePeople
Published in
5 min readDec 13, 2023

--

To perform concurrent operation using Kotlin Coroutine, we can simply do:

GlobalScope.launch {
// Code to do background work goes here
}

The above code is correct but not the right way of using Coroutines. Also using Coroutine is not as simple as that. We first have to understand what is Coroutine and how it is different from typical multithreading.

What is Coroutine:

Before understanding coroutine we should know what is suspend function: A suspending function is like a regular function but it can be suspended and resumed later. This keyword is Kotlin’s way to enforce a function to be called from within a coroutine.

Coroutines allow the execution of a block of code to be suspended and then resumed later(unlike multithreading in which we are dedicating tasks to a separate background thread), so that other work can be done in the meantime. Coroutines make it easier to write asynchronous code, which means one task doesn’t need to finish completely before starting the next task, enabling multiple tasks to run concurrently.

You can run many coroutines on a single thread due to support for suspension, which doesn’t block the thread where the coroutine is running. Suspending saves memory over blocking while supporting many concurrent operations.

How Coroutines are different from Java multithreading:
In java multithreading we are creating a new thread(possibly using a thread from a thread pool as creating a new thread each time is expensive) to run background tasks which unblocks the main thread. With Coroutines we are not necessary creating a new thread, we are using coroutine’s suspend functions which allows the other coroutines to use underlying thread.

Creating Coroutine:
We can create coroutines using launch and Async suspending functions.
launch: launch is a coroutine builder which launches a new coroutine without returning the result to the caller instead returns a reference to the coroutine as a Job.
async: aysnc also starts a coroutine and returns an object of type Deferred, which is like a promise that the result will be in there when it’s ready. You can access the result on the Deferred object using await() or awaitAll() for multiple coroutines.

//launch example with job (uncomplete code)
...
...
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("inside launch")
}
...

//async example with await(uncomplete code)
...
val res = async {
getData()
}
println(res.await())
...

This is not enough to work with coroutines. We first need to declare a scope.

Coroutine Scope:
All coroutines should run inside a scope to have structured concurrancy. Without a scope they might get lost and created a memory leak. If a scope fails with an exception or cancelled, all other children are cancelled too.
Also as a best practice all blocking function such as long running tasks or network calls should be marked as suspend. To make sure we call them from a coroutine to unblock the main thread.

Types of Coroutine Scopes:
runBlocking: This is a coroutine builder that bridges the non-coroutine world of a regular fun main() and the code with coroutines inside of runBlocking { … } curly braces.
Note: runBlocking is not a suspend function and the thread that called it remains inside it until the coroutine is complete.

coroutineScope: This a suspend function. If your coroutine inside this scope suspends, the coroutineScope function gets suspended as well. This allows the top-level function, a non-suspending function that created the coroutine, to continue executing on the same thread. The thread has “escaped” the coroutineScope block and is ready to do some other work.

import kotlinx.coroutines.*

// example of runBlocking and coroutineScope.
fun main() = runBlocking { //a non suspending function

println("before launching a coroutine")
launch{
delay(2000)
println("inside launch")
}
coroutineScope { // a suspending function
println("inside a scope")

launch{
println("inside launch before delay")
delay(1000)
println("inside launch after a delay")
}
}
println("after launching a coroutine")
}
// try running this code with and without delay on https://play.kotlinlang.org/

Custom scope: You can create your own coroutine scope as well using CoroutineScope (note: it is different from coroutineScope with small c) function. The most common way of using coroutine is following:

//Defining custom coroutine scope
private val scope = CoroutineScope(Dispatchers.Main + Job())
// The first parameter is explained later.
// second parameter Job() handles the cancellation of coroutines
...

scope.launch{
val result = repository.getItems() //Get some data from repoistory
}
...
scope.cancel()

GlobalScope: An application level coroutine, this is used rarely as coroutine are always related to some local scope.
There are limited circumstances under which `GlobalScope` can be legitimately and safely used, such as top-level background processes that must stay active for the whole duration of the application’s lifetime

Android built-in coroutine scope:
ViewModelScope
: This remain active when ViewModel is active and cleared when ViewModel is cleared.

// Calling View Model to get data in Fragment
Log.d("scope example", "before calling view model")
viewModel.getDataList()
Log.d("scope example", "after calling view model")


// inside ViewModel
fun getDataList() = viewModelScope.launch {
val res = repository.getItems() //network call to get data
}


// In the above example after getDataList() gets called,
// It goes back to Fragment for further execution and
// whenever we network call completes,we get the result.
// So not blocking the main thread.

LifecycleScope: LifecycleScope is defined for each lifecycle object. This coroutine is canceled when lifecycle is destroyed.
Use lifecycle.coroutineScope or lifecycleOwner.lifecycleScope to call coroutine.

LiveData: You can use liveData builder function to perform for example some asynchronous calculation. liveData scope starts executing when LiveData is active and cancelled when LiveData is inactive.

CoroutineContext: CoroutineContext is a map with key and value pair. It provide the information about the context in which Coroutine will be running. Examples of what a context can have:
- name — name of the coroutine to uniquely identify it
- job — controls the lifecycle of the coroutine
- dispatcher — dispatches the work to the appropriate thread
- exception handler — handles exceptions thrown by the code executed in the coroutine

Dispatcheres: Dispatchers determine which thread coroutine is going to use for its execution. Dispatchers are of following types:

Lets look at an example to check on which thread launch is working on:

// Calling View Model to get data in Fragment
Log.d("before calling view model", Thread.currentThread()+Dispatchers.toString())
viewModel.getDataList()
Log.d("after calling view model", Thread.currentThread()+Dispatchers.toString())

// inside ViewModel
fun getDataList() = viewModelScope.launch {
Log.d("viewModelScope", Thread.currentThread()+Dispatchers.toString())
val res = repository.getItems() //network call
}

Further reading:
Job() vs SupervisorJob()|
Kotlin Flows and Flow states
Android built-in scopes here: https://developer.android.com/topic/libraries/architecture/coroutines

--

--