Understanding Kotlin Coroutine: A Beginner's Guide
Introductory Overview: Kotlin Coroutines Made Easy for Beginners
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! ๐๐ฝ