Beginner’s Guide to Kotlin Coroutines

BipinBipin
4 min read

Kotlin coroutines are a powerful feature for handling asynchronous programming, making it easier to write clean, non-blocking code. Unlike traditional threads, coroutines are lightweight, allowing you to run thousands of them without overwhelming system resources.

Think of coroutines as a way to pause and resume tasks efficiently. They simplify complex asynchronous workflows by letting you write code that looks sequential, even though it runs asynchronously. In this blog, we’ll cover the essentials: launching coroutines, managing their lifecycle with scopes and jobs, ensuring structured concurrency, and handling cancellation.

CHAPTER 1 :

In this article , lets go through basics like runblocking, launch , delay suspensions, multiple launches inside runblocking and job join to wait for completion.


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("🚀 Main starts on thread: ${Thread.currentThread().name}") //1

    launch {
        println("🔹 Coroutine 1 starts on thread: ${Thread.currentThread().name}") // 3
        delay(1000)
        println("🔹 Coroutine 1 ends") //4
    }

    println("✅ Main continues while coroutine is working...") //2
}
  • runBlocking is used here for testing purpose and we need suspend function to run launch ( a suspending function ).

  • 1,2,3,4 are orders in which it will run.

  • After printing first line as it triggers launch, it doesn’t wait for whole delay time and pushes further to last print statement and then after delay completion 4 is triggered.

What if we have multiple launch ?

  •   fun main() = runBlocking {
          println("🚀 Main starts on thread: ${Thread.currentThread().name}") // 1
    
          // Launching multiple coroutines
          launch {
              println("🔹 Coroutine 1 starts on thread: ${Thread.currentThread().name}") //3
              delay(500)
              println("🔹 Coroutine 1 ends") //5
          }
    
          launch { // 4
              println("🔹 Coroutine 2 starts on thread: ${Thread.currentThread().name}") // 4
              delay(1000)
              println("🔹 Coroutine 2 ends") //6
          }
    
          println("✅ Main continues while both coroutines are working...") // 2
      }
    
  • What happens is after 1, first launch and second launch are triggered and quickly moves to last line.

  • Then 3 is triggered and delay suspends the first launch which navigates us to second launch and 4 is triggered.

  • Then 5 and 6 runs after that.

  • What happens is, it doesn’t wait for anything , its fast and moves on without blocking the flow.

    What happened above was fire and forget and run async. We can control it further by waiting for launch to finish using join or cancel it by calling cancel.

    Lets discuss further about it job , cancelling and join to wait :

    
      fun main() = runBlocking {
          println("🚀 Main starts on thread: ${Thread.currentThread().name}")
    
          // Launching multiple coroutines
          val coroutine1 = launch {
              println("🔹 Coroutine 1 starts on thread: ${Thread.currentThread().name}")
              delay(500)
              println("🔹 Coroutine 1 ends")
          }
    
          val coroutine2 = launch {
              println("🔹 Coroutine 2 starts on thread: ${Thread.currentThread().name}")
              delay(500)
              println("🔹 Coroutine 2 ends")
          }
    
          // Wait for both coroutines to finish
          coroutine1.join()
          coroutine2.join()
    
          println("✅ Main continues after both coroutines have finished.")
      }
    
      Result:
      ? Main starts on thread: main
      ? Coroutine 1 starts on thread: main
      ? Coroutine 2 starts on thread: main
      ? Coroutine 1 ends
      ? Coroutine 2 ends
      ? Main continues after both coroutines have finished.
    
    • What happens here is after hitting two launch, it was going to finish but sees join and hence waits for results to complete before moving to last print statement.

But lets see if there is one join only in case of two launch :

    fun main() = runBlocking {
        println("Main coroutine started.") // 1

        // Create a scope for our coroutines
        val job = launch {
            println("Launching coroutine 1...") // 2
            delay(1000) // Simulate work
            println("Coroutine 1 completed.") // 5
        }

        // Create another coroutine inside the same scope
        launch {
            println("Launching coroutine 2...") // 3
            delay(1500) // Simulate work
            println("Coroutine 2 completed.") //4
        }

        // Wait for the first job to complete before proceeding
        job.join()

        println("Main coroutine completed.") // 6
    }

    Result: 
    Main coroutine started.
    Launching coroutine 1...
    Launching coroutine 2...
    Coroutine 1 completed.
    Main coroutine completed.
    Coroutine 2 completed.
  • So ,first statement is printed. Then two launches gets triggered.

  • job.join waits for job to complete so 2 is triggered and then delay suspends us to second launch.

  • Hence 3 is triggered and after 1000 delay job 1 completes with 5.

  • Then doesn’t wait for second launch delay to complete, last line is printed 6 and finally second launch is completed with 4.

Lets cover structured concurrency, child and parent relationship scopes in next chapter.

0
Subscribe to my newsletter

Read articles from Bipin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bipin
Bipin