Understanding Kotlin Coroutine: A Beginner's Guide

Understanding Kotlin Coroutine: A Beginner's Guide

Introductory Overview: Kotlin Coroutines Made Easy for Beginners

ยท

7 min read

Hi, it's me Chandra Sekhar an Android enthusiast, in this article I will be sharing my knowledge about Kotlin coroutines.

To understand this topic you must familiar with basic Kotlin, hope you're already, if not click here ๐Ÿ‘ˆ๐Ÿฝ

Why does Kotlin Coroutine even exist? ๐Ÿง 

Why should I learn Kotlin coroutines ๐Ÿค”? Keep reading, and your question will be answered shortly! ๐Ÿค“

Nowadays, asynchronous programming is an essential part of coding. Kotlin made it easy for us to do asynchronous coding. Let's get to learning! ๐Ÿš€

Before understanding what Kotlin coroutines are, let's first understand why we need coroutines. We already have threads and callbacks for asynchronous coding, don't we? Technically, yes. However, working with threads and callbacks can be quite challenging. ๐Ÿ˜“ Allow me to provide you with a clear example of a simple network operation. ๐ŸŒ

fun fetchAndShowData(){
    val data = fetchData()
    showData(data)
}

fun fetchData() : Data {
// do some netwok operation
// return data
}

fun showData(data : Data){
// show data on UI
}

โš ๏ธ The above code will throw a NetworkOnMainThreadException โ—๏ธ, as doing network calls is not permitted on the main thread. ๐Ÿšซ

๐Ÿ”„ Hence, we will use a background thread โš™๏ธ for networking calls and a callback โฌ…๏ธ for returning the result.

fun fetchAndShowData() {
    fetchData { data ->
        showData(data)
    }
}
fun fetchData(callback: ((Data) -> Unit)) {
    // Performing network operation on a background thread
    // Got the data
    callback.invoke(data)
}
fun showData(data: Data) {
    // Show data on the UI
}

Suppose in a specific situation, our data relies on another piece of data or a key, which now needs to be fetched from the network ๐ŸŒ

fun fetchAndShowData(){
    fetchData { i ->
        fetchDataI(i) { j ->
            fetchDataJ(j) { k->
                fetchDataK(k) { data ->
                    showData(data)
                }
            }
        }
    }
}

Now this is something called Callback Hell. Even exception handling code is not added and this looks like a mess already.

Let's do the same with Coroutines ๐Ÿš€

Let me first write some code for you that you've already seen, but this time with coroutines:

fun fetchAndShowData() {
    GlobalScope.launch(Dispatchers.Main) {
        val i = fetchData()
        val j = fetchDataI(i)
        val k = fetchDataJ(j)
        val data = fetchDataK(k)
        showData(data)
    }
}

Wow, that's a massive difference, isn't it? ๐Ÿ˜ฎThis is the beauty of Kotlin Coroutines, my friend. Writing asynchronous code that looks exactly like synchronous โœจ

I believe you now have a general understanding of why we use coroutines. So, without any further delay, let's move on to learning the next phase ๐Ÿ˜€

What is kotlin-coroutine?

๐Ÿ”€ The word Coroutine is driven from Co = Cooperation and Routine = functions or computational sequences.

๐Ÿค Kotlin coroutines help different functions work together by letting them pause and continue their tasks. This makes efficient handling of asynchronous tasks, concurrency, and structured concurrency. โœจ

๐Ÿ”„ Threads and coroutines both work on asynchronous programming. The difference between them is a thread is managed by the OS ๐Ÿ‘ทโ€โ™‚๏ธ and a coroutine by the programmer ๐Ÿง‘โ€๐Ÿ’ป.

๐Ÿ’ก People often refer to coroutines as lightweight threads because they are built upon actual threads, utilizing the cooperative nature of functions to create a simple yet powerful framework. ๐Ÿš€

Here is some part from the official docs :

One can think of a coroutine as a lightweight thread. Like threads, coroutines can run in parallel, wait for each other and communicate. The biggest difference is that coroutines are very cheap, almost free: we can create thousands of them, and pay very little in terms of performance. True threads, on the other hand, are expensive to start and keep around. A thousand threads can be a serious challenge for a modern machine.

In a nutshell ๐Ÿฅœ we can say that with coroutines you can write code that looks sequential and synchronous, even though it runs asynchronously. This makes it easier to understand and maintain asynchronous code, especially for beginners who may find working directly with threads and callbacks challenging ๐Ÿ‘€

How to use Kotlin Coroutine?

Step 1. Add the following dependencies on your app module ๐Ÿ”Œ

dependencies {
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Step 2. Just use it ๐Ÿฑโ€๐Ÿ

Let us see the earlier network calling example, but this time with coroutine ๐Ÿ‘€๐Ÿš€

fun fetchAndShowData() {
    GlobalScope.launch(Dispatchers.Main) {
        val data = fetchData() // works on IO thread
        showData(data) // works on main thread
    }
}

suspend fun fetchData(): Data {
    return withContext(Dispatchers.IO) {
        // performing network operation on IO thread
        // return data
    }
}

fun showData(data: Data) {
    // show data on UI
}

Let's discuss it one by one ๐Ÿง˜๐Ÿฝโ€โ™‚๏ธ

GlobalScope is used to create a coroutine scope that is not bound to any particular lifecycle or context. It has a global lifetime and persists for the entire duration of the application.

In simple terms, it is advisable to avoid using GlobalScope and instead utilize lifecycleScope or viewModelScope, which is specifically tied to a particular scope or the lifecycle of a component.

launch is used to create a new coroutine, which means the text written before the launch function describes the scope (eg GlobalScope, lifecycleScope, viewModelScope), while the launch function itself is responsible for creating the coroutine.

Dispatchers help determine the thread in which the coroutine should run. There are three types of dispatchers -> Main, IO, Default.

  • Main -> Dispatchers.Main is the UI thread. It should be used for updating the user interface and interacting with UI-related components. In this coroutine- scope you can call suspend and non-suspend functions.

  • IO -> Dispatchers.IO is the background thread optimized for I/O-bound tasks. This is used for network calls and any kind of database-related operation.

  • Default -> Dispatchers.Default is the background thread suitable for CPU-intensive tasks, such as data processing or computation. It is designed for handling operations that consume more CPU resources like dataset filtering, etc.

suspend keyword indicates that the function fetchData is a coroutine function, which means it can be paused and resumed later without blocking the thread. A suspend can only be called from another suspend function or a coroutine scope.

withContext to specify the context in which the following code block should run. In this case, Dispatchers.IO is used. Keep in mind that withContext does not create a coroutine, it simply shifts the context of the existing coroutine.

That's it, I hope this satisfies you. If not, let's learn more about how to get the most out of it.

See the below code example, don't worry if you didn't understand ๐Ÿ‘€๐Ÿฅฝ

fun getAndShowData(){
    GlobalScope.launch(Dispatchers.Main){
        val res1 = getDataOne()
        val res2 = getDataTwo()
        showData(res1 + res2)
    }
}

suspend fun getDataOne() : String {
    return withContext(Dispatchers.IO){
        delay(3000) // Simulating a network request 
        return@withContext "Hello "
    }
}

suspend fun getDataTwo() : String {
    return withContext(Dispatchers.IO){
        delay(3000) // Simulating a network request
        return@withContext "World!"
    }
}

fun showData(data: String) {
    // show data on UI
}

P.S: Network operation simulated 3000 milliseconds for each.

The code above takes 6000 milliseconds โณ to complete the task. However, that doesn't seem ideal! I wonder, can't I call both functions and wait for the results? Of course, we can! ๐Ÿ˜‹

There is another function called async ๐Ÿš€, unlike launch, it returns a Deferred object. We can then use await() โณ on each Deferred object (returned by async) to suspend the coroutine until the corresponding task is completed. This way, we can efficiently wait for the results without blocking the main thread.

What is the benefit of using async? ๐Ÿค”

Here is the same, but with async await โŒ›

fun getAndShowData(){
    GlobalScope.launch(Dispatchers.Main){
        val res1 = async { getDataOne() }
        val res2 = async { getDataTwo() }
        val finalRes = res1.await() + res2.await()
        showData(finalRes)
    }
}

suspend fun getDataOne() : String {
    return withContext(Dispatchers.IO){
        // Simulating a network request 
        delay(3000)
        return@withContext "Hello "
    }
}

suspend fun getDataTwo() : String {
    return withContext(Dispatchers.IO){
        // Simulating a network request 
        delay(3000)
        return@withContext "World!"
    }
}

fun showData(data: String) {
    // show data on UI
}

By utilizing async, we can significantly reduce the total time โŒ› taken for the network requests since they can execute concurrently. In this case, instead of waiting for 6000 milliseconds, we only need to wait for the longer of the two requests (3000 milliseconds) before obtaining the final result. ๐Ÿš€

The line val res1 = async { getDataOne() } indicates that getDataOne() will be called, and a Deferred object will be returned. We can use await() on this object to wait for the result.

This line val finalRes = res1.await() + res2.await() means it'll not proceed further. The await() function suspends the coroutine's execution until the deferred tasks are completed and their results are returned. Once both results are available, the concatenation operation will be performed, and only then the line showData(finalRes) will be executed.

Hence the conclusion :

  • launch: fire and forget ๐Ÿค”

  • async: perform a task and return the result ๐Ÿง 

I hope it's more than enough for someone to get started with coroutines. Don't hesitate to explore and use coroutines in your projects, connect with me {click here} if you face any issues. Happy coding, and until next time, Sayonara! ๐Ÿ‘‹๐Ÿฝ

ย