Best practices using Coroutines in Kotlin

Aryan Gupta
Bobble Engineering
Published in
5 min readDec 8, 2022

Like any other techniques Coroutines are equally important for building a new feature or an app which overall deals with making the code more stable, concise and clear, and executing the highest priority functions first. I will be sharing links with points where it’s required so that, you must get a proper idea of how and where to use it. It is advised to read the documentation. So, with that said, let us start…

Let’s check on some more facts for coroutines:

  • Light-weight threads but not the same as thread.
  • Like threads, coroutines can run in parallel, wait for each other, and communicate with each other.
  • Very cheap, or almost free. Creates thousands of them without any memory issues.
  • On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive.
  • Enable to write more cleaner and concise app code.

Fact: Over 50% of professional developers who use coroutines have reported seeing increased productivity.

But why coroutines, and why not threads?

  • They are expensive to keep
  • Expensive to switch
  • High-load server side

Since, while using threads, “Application not responding” or ANR comes up very frequently to the application, as when multiple works are given to the application, so until one completes another fail to get executed, hence coroutines come as rescue.

Threads usability and disadvantages:

fun main() {        //executes in main thread
println("Main program starts: ${Thread.currentThread().name}")
thread { //creates a background thread (worker thread)
println("Fake work starts: ${Thread.currentThread().name}")
Thread.sleep(1000) //Pretend doing some work....may be file upload
println("Fake work finished: ${Thread.currentThread().name}")
//this statement will executed after 1 sec, because of the fake work going on at the above line of code
}
println("Main program ends: ${Thread.currentThread().name}")
}

Now what really happens in the background is when we call the first print, it gets executed, but when the threads must get executed, it gets delayed, and another function got executed. In the case of threads, the application waits for all the background threads to complete. So, in general, it consumes time and is not lightweight.

Impact by Coroutines:

fun main() = runBlocking {
println("Main program starts: ${Thread.currentThread().name}") //main thread
GlobalScope.launch { //Thread: T1
println("Fake work starts: ${Thread.currentThread().name}") //Thread: T1
delay(1000) //Coroutine is suspended but Thread: T1 is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}") // Either T1 or some other thread
}
delay(2000) //main thread: wait for coroutine to finish (practically not a right way to wait)
println("Main program ends: ${Thread.currentThread().name}") //main thread
}
  • Unlike threads, for coroutines, the application by default does not wait for it to finish its execution.
  • GlobalScope.launch() is non-blocking in nature whereas runBlocking() blocks the thread in which it operates.
  • Global Coroutines are top-level coroutines and can survive the entire life of the application.
  • Using GlobalScope is highly discouraged, because if we forget to close GlobalScope then it will continue consuming memory. [USE ONLY WHEN NEEDED]

Coroutines Builders:

‘launch’ coroutine builder (Fire and Forget):

fun main() = runBlocking {
println("Main program starts: ${Thread.currentThread().name}")
val job: Job = launch {
println("Fake work starts: ${Thread.currentThread().name}") //Thread: T1
mySuspendFunc(1000) //Coroutine is suspended but Thread: T1 is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}")
}
job.join()
println("Main program starts: ${Thread.currentThread().name}")
}
  • launch is essentially a Kotlin coroutine builder that is “fire and forgets”. This means that ‘launch creates a new coroutine that won’t return any result to the caller’.
  • Launches a new coroutine without blocking the current thread and inherits the thread & coroutine scope of the immediate parent coroutine.

‘async’ coroutine builder:

fun main() = runBlocking {
println("Main program starts: ${Thread.currentThread().name}")
val jobDeferred: Deferred<Int> = async {
println("Fake work starts: ${Thread.currentThread().name}") //Thread: T1
delay(1000) //Coroutine is suspended but Thread: T1 is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}")
}
val num:Int = jobDeferred.await()
println("Main program starts: ${Thread.currentThread().name}")
}
  • While current thread is working, it launches a new coroutine without blocking it.
  • Inherits the thread & coroutine scope of the immediate parent coroutine.
  • Using Deferred object, you can cancel the coroutine, wait for coroutine to finish, or retrieve the returned result.
  • Returns a reference to the Deferred<T> object and Deferred<T> is subclass of Job.

Composing Suspending Functions:

  • Sequential Execution- Function execution is sequential by default
fun main() = runBlocking {    // Creates a blocking coroutine that executes in current thread (main)
val time = measureTimeMillis {
val msgOne = getMessageOne()
val msgTwo = getMessageTwo()
println("The entire message is: ${msgOne + msgTwo}")
}
}
suspend fun getMessageOne(): String {
delay(1000L) // pretend to do some work
return "Hello "
}
suspend fun getMessageTwo(): String {
delay(1000L) // pretend to do some work
return "World!"
}
  • Concurrent Execution- Achieve concurrent execution by async{} or launch as child [Runs in parallel, takes lesser time than sequential execution]
fun main() = runBlocking {    // Creates a blocking coroutine that executes in current thread (main)
val time = measureTimeMillis {
val msgOne: Deferred<String> = async {
getMessageOne1()
}
val msgTwo: Deferred<String> = async {
getMessageTwo2()
}
//both the calls runs in parallel, hence the time taken to execute is lesser that sequential suspending
println("The entire message is: ${msgOne.await() + msgTwo.await()}")
}
println("Completed in $time ms")
}
suspend fun getMessageOne1(): String {
delay(1000L) // pretend to do some work
return "Hello "
}
suspend fun getMessageTwo2(): String {
delay(1000L) // pretend to do some work
return "World!"
}
  • Lazy Coroutine Execution- Lazily execute code in coroutine
fun main() = runBlocking {    // Creates a blocking coroutine that executes in current thread (main)
val msgOne: Deferred<String> = async(start = CoroutineStart.LAZY) { getMessageOne11() }
val msgTwo: Deferred<String> = async(start = CoroutineStart.LAZY) { getMessageTwo22() }
println("The entire message is: ${msgOne.await() + msgTwo.await()}")
}
suspend fun getMessageOne1(): String {
delay(1000L) // pretend to do some work
println("After working in getMessageOne()")
return "Hello "
}
suspend fun getMessageTwo2(): String {
delay(1000L) // pretend to do some work
println("After working in getMessageTwo()")
return "World!"
}

Check out: Kotlin coroutines on Android | Android Developers

So, now you know, how can we use Coroutines, implement and try it yourself, and you will surely get amazed by the experience. Hope these tips will help you while developing and making your code cleaner and more concise. Please do comment if you know some other ways and you want me to write on that and give a clap if you find this story useful.

--

--

Aryan Gupta
Bobble Engineering

Android App Developer | Web Developer | Data Structures and Algorithms | Competitive Programming