Kotlin Coroutines for Beginners

Abhishek Pathak
7 min readSep 24, 2022

--

Building Blocks of coroutines

  1. What are Coroutines?
  2. Suspend functions.
  3. Coroutine Builders.
  4. Coroutine Dispatchers.
  5. Coroutine Scope.
  6. Coroutine Jobs.
  7. Cancelling Jobs
  8. Error handling in Coroutines.

What are Coroutines?

Coroutines is a concurrency design pattern than simplify code by executing asynchronously.

A coroutine is not a Thread. Coroutines are “light weight threads”.

What does mean of “light weight threads” ?

Coroutines are NOT a replacement of threads. They only provide us with a framework to efficiently manage the good, old thread that we are already familiar with. Threads are limited in number but we can have as many coroutines as we want. Threads are managed by the OS while as the coroutines are managed by the user. Multiple coroutines can run on a single thread, such that one thread is utilized in a far more efficient manner. This is helpful when a thread is sitting idle and can execute a few lines of another function, instead of just being a couch potato!

In other words, Coroutines are less resource-intensive than JVM threads.

why????

Code that exhausts the JVM’s available memory when using threads can be expressed using coroutines without hitting resource limits. For example, the following code launches 50000 distinct coroutines that each wait 4 seconds and then print a period (‘Abhishek Pathak’) while consuming very little memory:

fun main() = runBlocking {
repeat(500000) { // launch a lot of coroutines
launch {
delay(5000L)
print("Abhishek Pathak")
}
}
}

Response

Thread VS Coroutine

With threads, the operating system switches running threads preemptively according to its scheduler, which is an algorithm in the operating system kernel. With coroutines, the programmer and programming language determine when to switch coroutines; in other words, tasks are cooperatively multitasked by pausing and resuming functions at set points, typically (but not necessarily) within a single thread.

Suspend Functions

Asuspending function is simply a function that can be paused and resumed at later time.

The syntax of a suspending function is similar to that of a regular function except for the addition of the suspend keyword. It can take a parameter and have a return type. However, suspending functions can only be invoked by another suspending function or within a coroutine scope.

viewModelScope.launch {
try {
loading.postValue(true)
val response = repository.getRemoteData()
loading.postValue(false)
if (response.isSuccessful) {
response.body()?.let {
weatherLiveData.postValue(it)
}
}
} catch (e: Exception) {
loading.postValue(false)
error.postValue(e.toString())
}
return@launch
}

Coroutine Builders

  • Launch: is a fire-and-forget type of coroutine that does not return any value.
viewModelScope.launch(Dispatchers.IO) {
try {
loading.postValue(true)
val response = repository.getRemoteData()
loading.postValue(false)
if (response.isSuccessful) {
response.body()?.let {
weatherLiveData.postValue(it)
}
}
} catch (e: Exception) {
loading.postValue(false)
error.postValue(e.toString())
}
}
  • Async: async{}returns an instance of Deferred<T>, which has an await()function that returns the result of the coroutine like we have future in Java in which we do future.get() to the get the result.
// Task - Fetch data from 2 diffrent IO Jobs parallel way and then combine the 
// result using main dispacher
GlobalScope.launch(Dispatchers.Main) {
// here JOB 1 and JOB 2 are parallel jobs
val firstApiCall = async(Dispatchers.IO) { fetchFirstData() } // Job 1
val secondApiCall = async(Dispatchers.IO) { fetchSecondData() } //job 2
showTotalData(firstApiCall.await(), secondApiCall.await()) // back on UI thread
}
  • RunBlocking is a coroutine function. By not providing any context, it will get run on the main thread. Runs a new coroutine and blocks the current thread interruptible until its completion. This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in unit testing classes in Android development.
fun callRunBlockingSample(){
Log.d("TAG","Before run-blocking")
runBlocking{
Log.d("TAG","just entered into the runBlocking ")
delay(5000)
launch(Dispatchers.IO)
{
delay(3000L)
Log.d("TAG","Finished to coroutine 1")
}
launch(Dispatchers.IO)
{
delay(3000L)
Log.d("TAG","Finished to coroutine 2")
}
Log.d("TAG","start of the run-blocking")
Log.d("TAG","End of the runBlocking")
}
Log.d("TAG","after the run blocking")
GlobalScope.launch{
Log.d("TAG","Logging in the globalScope")
}
}

Few noted things about “withContext ” use-cases

Use withContext when you do not need the parallel execution.

Use async only when you need the parallel execution.

Both withContext and async can be used to get the result which is not possible with the launch.

Use withContext to return the result of a single task.

Use async for results from multiple tasks that run in parallel.

Coroutine Dispatchers

Dispatcher basically determines which thread or thread pool the coroutine runs on. So you don’t necessarily need to define a dispatcher if you don’t want to

  • Main Dispatchers: we will use the Main dispatcher when we want to update the UI from liveData, or Presenter view method etc.
runBlocking {
launch(Dispatchers.Main) {
Log.d(TAG, "Main dispatcher. Thread: ${Thread.currentThread().name}")
}
}
  • Default Dispatchers: Useful for performing CPU intensive work like searching, sorting or filtering of data.
runBlocking {
launch(Dispatchers.Default) {
Log.i(TAG, "Default. Thread: ${Thread.currentThread().name}")
}
}
  • IO Dispatchers: Useful for network communication or reading/writing files, Making API call or storing to Database.
runBlocking {
launch(Dispatchers.IO) {
Log.i(MainActivity.TAG, "IO. Thread: ${Thread.currentThread().name}")
}
}
  • Unconfined Dispatchers: Starts the coroutine in the inherited dispatcher that called it
launch(Dispatchers.Unconfined) {
Log.i(TAG, "Unconfined1. Thread: ${Thread.currentThread().name}")
delay(100L)
Log.i(TAG, "Unconfined2. Thread: ${Thread.currentThread().name}")
}

Coroutine Scope

In Kotlin, Coroutines must run in something called a CoroutineScope. A CoroutineScope keeps track of your coroutines, even coroutines that are suspended. To ensure that all coroutines are tracked, Kotlin does not allow you to start a new coroutine without a CoroutineScope.

There are basically 3 scopes in Kotlin coroutines:

Global Scope: When Coroutines are launched within the global scope, they live long as the application does. Coroutines launched in the global scope will be launched in a separate thread.

GlobalScope.launch {
Log.d(TAG, Thread.currentThread().name.toString())
}
Log.d("Outside Global Scope", Thread.currentThread().name.toString())

LifeCycle Scope: The lifecycle scope is the same as the global scope, but the only difference is that all the coroutines launched within the activity also die when the activity dies.

// launching the coroutine in the lifecycle scope
lifecycleScope.launch {
while (true) {
delay(1000L)
Log.d(TAG, "Still Running..")
}
}
GlobalScope.launch {
delay(5000L)
val intent = Intent(this@MainActivity, SecondActivity::class.java)
startActivity(intent)
finish()
}

ViewModel Scope: The coroutine in this scope will live as long the View Model is alive.

viewModelScope.launch {
//doSomething
}

Coroutine Job

A Job is a cancellable thing with a life-cycle that culminates in its completion. Coroutine job is created with launch coroutine builder. It runs a specified block of code and completes on completion of this block.

  • A Job can be used to wait for the coroutine to do some work or it can be used to cancel it.
  • We can also pass a Job to a CoroutineScope to keep a handle on its lifecycle.
  • Coroutines can be controlled through the functions that are available on the Job interface.

Some Jobs states are below:

start()

join()

cancel()

// A job is returned bu the builder block
val job = GlobalScope.launch(Dispatchers.Default) {
}

Job Cancelling

cancel() method is used to cancel the coroutine, without waiting for it to finish its work.

fun playWithCoroutineJobCancellation(){
val job = GlobalScope.launch(Dispatchers.Default) {
repeat(5)
{
Log.d("TAG", "Coroutines is still working")
delay(1000)
}
}
runBlocking {
// delaying the coroutine by 2sec
delay(2000)
// canceling/stopping the coroutine
job.cancel()
Log.d("TAG", "Main Thread is Running")
}
}

Exception Handling in Kotlin Coroutines

Exception handling can be done via 2 diffrent ways.

Way 1 : Using general try catch way to handle it but this way is not really special for coroutines.

GlobalScope.launch(Dispatchers.Main) {
try {
doLogin() // do on IO thread and back to UI Thread
} catch (exception: Exception) {
Log.d(TAG, "$exception caught here !")
}
}

Way 2 : Using CoroutineExceptionHandler that is more preferred way of handling exception in coroutines like below snippet.

//How to do declaration
val handler = CoroutineExceptionHandler { _, exception ->
Log.d("TAG", "$exception caught here !")
error.postValue("$exception caught here !")
}
fun loadWeather() {
val job = viewModelScope.launch(Dispatchers.IO + handler) {

loading.postValue(true)
val response = repository.getRemoteData()
loading.postValue(false)
if (response.isSuccessful) {
response.body()?.let {
weatherLiveData.postValue(it)
}
}
}
}

Thanks for reading this article. Be sure to click 👏 below to applause this article if you found it helpful. It means a lot to me.

--

--