Kotlin Coroutines

Introduction

Many times one may want to perform long-running tasks, like fetching data from a database or even uploading data, which often take so much time to be completed, what can I do so that even as I perform this long-running task I can continue with other processes? well, coroutines it is.

So!! in this article we'll be talking about coroutine and how we can make use of it. Let's get started...

What is a coroutine, why coroutines, and how to use coroutine?

What is Coroutine?

  • Coroutine is an instance of suspendable computation,
  • It is similar to thread but its lightweight,
  • It is not bound in any thread and it can tie its execution in one thread and continue on the other threads.

Example of a coroutine block of code

    fun main() = runBlocking { // this: CoroutineScope
       launch { onStop() }
       println("Here...there")

   }

   suspend fun onStop() {
      delay(1000L)
      println("Stopped!")
   }
  • runBlocking is a coroutine scope that bridges the non-coroutine world.
  • launch is a coroutine builder that launches new coroutines with other code concurrently
  • delay is a suspend function that suspends the coroutine for a specified time.

  • Why coroutines: -

    • It is less resource intensive as compared to Java threads.
    • it prevents the callback problem.
    • Easy to maintain and control
    • Gives a controllable way to handle code

      How to use coroutines

      fun main() { 
      println("Work started ${Thread.currentThread().name}")  //This will print the string with the thread on which it is running.
      
      GlobalScope.launch{onStop(name = "1")}
      CoroutinScope(Dispatcher.Io).launch { onStop("2") }
      
      Thread.sleep(4000)
      
      }
      
      suspend fun onStop(name:String) {
      delay(1000L)
      println("Stopped!")
      }
      

Jobs

  • Jobs are a handle to a coroutine.
  • It Models the lifecycle of a coroutine
  • Job also acts as the root of the coroutine hierarchy in the coroutine scope

Job Lifecycle

The job lifecycle is usually in the following states:

  • New - Created and not started.
  • Active - started but not completed
  • Completing - waiting for children to complete
  • Completed - finished and completed children
  • Cancelling - cancel is called or exception is thrown.,
  • Cancelled - cancel is completed

These states cannot be accessed directly but rather we use the Properties that are related to the states.

  • isActive - The coroutine is in this state when it has been started, one it is running its operation it moves to the completing state
  • isCompleted - The coroutine is moved to completed once all its children have completed the task.
  • isCancelled - The coroutine is in a canceling state when a cancel is called or an exception is thrown once the cancel is completed it is moved to isCancelled.

Example

fun main()= runBlocking{
   val job = GlobalScope.launch{
      var num = 1
      while(isActive){
         println("The number is:${num++}")
      } 
   }
   delay(20)
   job.cancel()
}

Coroutine Context

A coroutine context is used to define the behavior of a coroutine and it contains a set of elements

  • CoroutinDispatcher which defines Threading and by Default it is Dispatchers. Default
  • a job that defines lifecycle and by default it is No parent job
  • CoroutinExceptionHandler by default is None
  • CoroutineName by default is coroutine
parentContext = Defaults + inherited context + passed arguments

coroutineContext = parentContext + job()

What is context switching

It is to change the context in which a coroutine runs. the context is switched using the withContext()

Example

fun main() = runBlocking{
   doBgTask()
}

suspend fun moveToForground(){
   withContext(Dispatchers.Main){
      ...
   }
}

suspend fun doBgTask(){
   withContext(Dispatchers.IO){
      moveToForground()
   }
}

Builder

These are the operations that start the coroutines.

  • Act as a 'CoroutineScope' extension function
  • Increases control over the lifecycle and behavior of coroutines.
  • Returns a job

Type of builder

  • launch which is used when there is no result expected.
  • await which is used when there is a result expected.
fun main  = runBlocking{
   val job1 = GlobalScope.launch{
      println("Has already started...")
   }

   val job2 = GlobalScope.async{
      println("Starting Here and now....")
      delay(1000)
      "Okay: Am done!!"
   }
   println(job1)
   println(job2)
   println("-".repeat(20))
   println(job2.await())
   Thread.sleep(2000)
}

Output

Has already started...
"coroutine#2":StandaloneCoroutine{Completed}@71e7a66b
"coroutine#3":DeferredCoroutine{Active}@2ac1fdc4
--------------------
Starting Here and Now...
Okay: Am done!!

CoroutineScope

What does coroutine scope do?

  • Defines the scope of a new coroutine.
  • keep track of the coroutine
  • Give the ability to cancel the ongoing work
  • Notifies when a failure happens.

Every coroutine builder is an extension of a coroutine scope and inherits its coroutineContext to automatically propagate all its elements.

What is the best way to obtain an instant of a scope?

  • CoroutineScope() - These uses the context provided to it as a parameter and adds a job if one is not provided as part of the context
  • MainScope() - uses Dispatcher.Mainfor its coroutine and has a SupervisorJob
  • lifecycleScope - Tied to the lifecycle state of the component. Either activity of the fragments.
  • viewModelScope - Tied to the lifecycle of the viewmodel.
 val scope = CoroutinScope(Dispatchers.Main)

 // launching a coroutine
 val job = scope.launch{
      sing()
 }
//canceling a coroutine
scope.cancel()
 suspend fun sing(){
   println("Hallelu...jah.....!!!")
 }

Dispatchers

  • Coroutine context is a set of elements
  • The main element of a coroutine context is the job of the coroutine and the dispatcher

So what is a Dispatcher?

  • Dispatcher is what determines the thread or the threads that a corresponding coroutine uses for its execution.
  • it restricts a coroutine execution to a specific thread, dispatches it to a thread, or lets it run unrestricted.

Properties of Dispatchers

  • Dispatchers. Default

    • Is used by all standard builders if no dispatcher is specified in their context.
    • It uses a common pool of shared background threads.
    • Is considered as the appropriate choice for coroutine computations that consume CPU resources.
  • Dispatchers.IO

    • uses a shared pool of on-demand created threads
    • it is designed for offloading of IO-intensive blocking operations
  • Dispatchers. Unconfined

    • This starts coroutine execution in the current call frame until the first suspension that the coroutine builder returns.
    • This should not be normally used in code.
  • Dispatchers. Main

    • Used for UI operations and Non-blocking code.
   fun main = runBlocking{
      withContext(Dispatchers.Main){...}
      withContext(Dispatchers.Default){...}
      withContext(Dispatchers.IO){...}
      withContext(Dispatchers.Unconfined){...}
   }

What is context switching?

Context switching is to change the context in which a coroutine runs.

  • Example

    fun main() = runBlocking{
       doNetworkStuff()
    }
    
    suspend fun updateUi(){
       withContext(Dispatchers.Main){
          ...
       }
    }
    
     suspend fun doNetworkStuff(){
       withContext(Dispatchers.IO){
          ...
          updateUi()
       }
    }
    

    Conclusion

    In conclusion, Kotlin Coroutines have revolutionized the way developers handle asynchronous programming in Android applications. With their simple syntax, lightweight structure, and seamless integration into existing codebases.

    Reference

    Official Documentation

    MamboBryan Decks

    Kotlin conf2019