Coroutine Basics

Chetan Bhalerao
6 min readNov 28, 2022

--

I have been using Coroutine since so many days but never get a chance to finish this blog. We will try to cover some basic stuff in this blog and will go deep in upcoming blogs.

What is Coroutine?

Coroutine is nothing but a light weight thread to execute various tasks.

Let’s take a example, we have to make 1 network call, 1 database operation, and 1 complex calculation of mathematical operation to execute.

How are we going to do this?

By creating various threads, hmm answer is somewhat correct but what if we have thousands of operations to execute like above, will creating thousands of threads be possible without impacting the system? The answer is no of course, what other options do we have?

For this kind of situation, Kotlin provides a simple solution, which is coroutines.

A coroutine is trying to solve the complexity of callback hell, which is a common side-effect of asynchronous tasks. Coroutine provides the best of both worlds i.e concurrency and synchronous flow of code which provide readability and ease to understand.

Advantages of coroutines

  • Can have thousands of coroutines in single thread
  • Coroutines can be cancel, pause and resume
  • Coroutine has a scope, if set according to usecase can help in preventing memory leaks.
  • Coroutine provide exception handling in case of failures
  • Improves the readability by avoiding callback functions.

In a coroutine, the scope of the coroutine is pretty important which helps in setting the boundary for that coroutine to execute, suppose the scope of the coroutine finishes due to any reason, the coroutine within that scope finishes. This really helps in avoiding side effects on the system.

Types of CoroutineDispatchers

  1. IO (Network & disk) — Deals with network, file writing and reading
  2. Main (UI-related stuff) — Main dispatcher allows to perform actions on UI like, setting text value or doing animations etc.
  3. Default (CPU intense work) — Complex calculations, like mathematical operations
  4. Unconfined — Never get a chance to use this.

Suspend functions

  • Suspend function always runs in scope.
  • When suspend function returns it has finished all its work.
  • Whenever the scope gets canceled, all coroutine children of that scope get canceled

If function is calling any suspend function we have to mark the calling function as suspend. In the above example if fetchUserProfile calls any other function we have to mark that as a suspend. While onLoadLayout is not a suspend function as it triggers the coroutine using the launch.

CoroutineScopes

  • Keep track of coroutines
  • Ability to cancel them
  • Notified when failure happens

CouroutineScope needs a dispatcher for creation, it takes scope according to the nature of the task we want to perform like a network call or doing some action on UI.

val scope = CouroutineScope(Dispatchers.io)
fun onLoadLayout(){
scope.launch {
fetchUserProfile()
}
}
suspend fun fetchUserProfile(){
// network call to get user profile
delay(2000L)
}
val scope = CouroutineScope(Dispatchers.io)
scope.launch {
launch {
println("In coroutine 1")
}

launch {
println("In coroutine 2")
}

launch {
println("In coroutine 3")
}

launch {
println("In coroutine 4")
}
}

One interesting fact about CoroutineScope, if any of the children's coroutine throws an exception, it will propagate to the parent and if it is not handled correctly app will crash. Let's check the above code, suppose we created one CoroutineScope, and in that scope, we created 4 coroutines, now 2nd coroutine throws an exception. In that case, coroutine3 and coroutine4 will not execute and an exception will propagate to the parent coroutine.

Commonly used scopes in coroutine

  • GlobalScope — This scope exists throughout the application life span after launching till the app is killed, we should use this scope only if you have a task that is not dependent on any activity or view. We should avoid using this scope when we have tasks specifically related to an activity, or fragment. Let’s take an example, suppose we have a network call that fetches user information after opening activity, using a global scope, we are fetching user details and setting the value in text view. Now due to some reason network calls take a long time and in between user closes the application using the back button the activity is no more available while the application is still not killed. In this situation, there is a chance of a memory leak. To avoid this we should avoid GlobalScope and be more specific.

Example of GlobalScope

fun main(){
GlobalScope.launch(Dispatchers.Default) {
task()
}
}
fun task(){
Log.d("task", "Executing task function..")
}
  • LifecycleScopeAs name suggest this scope follows the lifecycle and try to keep coroutine in lifecycle of activity or fragment. This prevents the memory leak scenarios. Checkout below example for coroutine.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("LifecycleScope handler", "exception occured " + throwable.localizedMessage)
}
lifecycle.coroutineScope.launch(handler) {
task1()
}
}

suspend fun task1(){
delay(3000)
Log.i("task1", "running..")
}
}

Lifecycle scope provide various options of launch as follows

  1. launchWhenCreated {…} gets suspended when onDestroy() is called or user clear the app from backStack
  2. launchWhenStarted {…} gets suspended when onStop() is called
  3. launchWhenResumed {…} gets suspended when onPause() is called

We can use these options according to our usecase requiment.

LifecycleScope uses SupervisorJob. In the SupervisorJob, if any child coroutine failed then this failure is not propagated to any other coroutines. But if the parent gets canceled then all the children get canceled.

Lifecycle scope accept the handler while launching, which handles exception while executing function inside lifecycle flow. Handler is optional though

  • ViewmodelScopeViewmodelScope especially designed for ViewModel purpose. Scope started when ViewModel initiated and scope is automatically canceled if the ViewModel is cleared.

This ViewModelScope has Dispatchers.Main dispatcher, that means by default all code will be executed in the Main thread. And it uses the SupervisorJob. So the exception handling of ViewModelScope is the same as the LifecycleScope we have just learned before.

Example:

class MyViewModel : ViewModel() {
init {
viewModelScope.launch {
//task
}
}
}

Async and await

Async and await as name suggest helps in running code asynchronously instead of running part of code one after another which doesn’t make any sense in most of the cases.

Suppose we have 2 network calls to execute but both network calls are independent, in that case both network calls can be run in parallel.

fun main(){
GlobalScope.launch(Dispatchers.Default) {
networkCall1()
networkCall2()
}
}
suspend fun networkCall1(): String{
Log.d("networkCall1", "Executing 1st network call..")
delay(2000l)
return "call1"
}
suspend fun networkCall2(): String{
Log.d("networkCall1", "Executing 1st network call..")
delay(2000l)
return "call2"
}

In above example total time taken to execute both network call will be more than 4000ms as networkCall1 will complete its execution and then networkCall2 will start its execution.

To tackle this scenario, Coroutine provides async and await.

fun main(){
GlobalScope.launch(Dispatchers.Default) {
val async1 = async {
networkCall1()
}
val async2 = async {
networkCall2()
}
val stringFirst = async1.await()
val stringSecond = async2.await()
Log.d("async test", "networkCall1 result: $stringFirst")
Log.d("async test", "networkCall2 result: $stringSecond")
}
}
suspend fun networkCall1(): String{
Log.d("networkCall1", "Executing 1st network call..")
delay(2000L)
return "call1"
}
suspend fun networkCall2(): String{
Log.d("networkCall1", "Executing 1st network call..")
delay(2000L)
return "call2"
}

When the async function is called it returns a Deferred value,

What is Deferred?

Deferred in nothing but a job, with non-blocking cancellable future value. In simple terms, we will get a value which suspend function returns once the async task is finished. In our example, we will get Deferred<String>.

Awaits for completion of this value without blocking the thread and resumes when deferred(Job) computation is complete. It can return the exception in case the deferred(Job) is canceled due to any reason.

In the above example now both network calls will start parallel and will take a little bit more than 2000ms. which will solve our purpose.

Testing with coroutine

Testing can be tricky with coroutines as it executes asynchronously. To solve this dilemma coroutine library provides the runBlocking method.

runBlocking Runs a new coroutine and blocks the current thread interruptibly until its completion. This function should not be used from a coroutine.

runBlocking { 
fetchUserProfile()
fetchAnotherProfile()
}

In the above example fetchUserProfile() task will be executed synchronously and once fetchUserProfile() is finished then only fetchAnotherProfile() will get called.

This is just basic idea of coroutines and why it is used, in upcoming blog we will cover network call with coroutines and view model.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Chetan Bhalerao
Chetan Bhalerao

Written by Chetan Bhalerao

Android developer, Backend developer. Interested in exploring new technologies. https://twitter.com/csbhalerao

No responses yet

Write a response