Coroutines


Coroutines are not just a kotlin-specific concept, it's an Legacy concept.

There are no Coroutines in Java, but Kotlin has.

Coroutine is the structure used for asynchronous operations in long-running tasks and network operations or not to block UI.

Coroutines are also called light threads. Less energy, more work compared to threads (coroutines are in the thread pool, of course).



Code Example


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch {
            repeat(100_000){          //we created coroutine 100.000 time
                launch {
                    println("Coroutines are working!")
                }
            }
        }

    }
}

Output: 
Coroutines are working!
Coroutines are working!
Coroutines are working!
Coroutines are working!
Coroutines are working!
//...(100000 time)


Scope: Code written inside a code block is under a scope. The codes do not work in other scopes, there is no link.

Scope in Coroutines: Scopes that determine where coroutines will be run and their lifecycle.

  • Global Scope: The scope in which the whole application can be run within the application. we don't come across it very often because using a coroutine in the whole application often makes no sense.

  • runBlocking: Creating a scope by blocking. It stops the subsequent codes from running until the previous operations are finished. This doesn't come across much. It can be done if we have only one purpose, such as running a coroutine. Most of the time it doesn't make sense to block other code.

  • CoroutineScope: A scope is created and continues to run until all coroutines in it are finished. It is used frequently.

  • -Nested scopes can be used within each other.

    -launch keyword: Starts a new coroutine and does not block whatever current thread it contains.

    -We can start and change as many "launch" as we want.


    runBlocking:

    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            println("run block started")
            runBlocking {
                launch {  //start coroutine
                    println("run blocked")
                    delay(6000)
                }
            }
            println("run block ended")
    
        }
    }
    
    
    Output: 
    2022-09-11 14:56:59.279 9767-9767/me.king.androidApp I/System.out: run block started
    2022-09-11 14:56:59.306 9767-9767/me.king.androidApp I/System.out: run blocked
    2022-09-11 14:57:05.309 9767-9767/me.king.androidApp I/System.out: run block ended
    
    



    global scope:

    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            println("run block started")
            GlobalScope.launch {
                delay(6000)
                println("global scope")
            }
            println("run block ended")
    
        }
    }
    
    
    Output: 
    2022-09-11 15:01:35.010 9932-9932/me.king.androidApp I/System.out: run block started
    2022-09-11 15:01:35.045 9932-9932/me.king.androidApp I/System.out: run block ended
    2022-09-11 15:01:41.050 9932-9997/me.king.androidApp I/System.out: global scope
    

    -Runblocking and global scope are different constructs. In run blocking, the process of the code block under launch, which is blocked, is expected to finish, then other operations are performed. In global scope, the process under launch runs in the background, processes that are due continue the processes.




    coroutineScope:

    It is used in 2 ways. Suspend can be called within a function or in other scopes (which can be run with other coroutines).

    like;

    
     GlobalScope.launch {
                coroutineScope { 
                    ...
                } 
            }
    

    What contexts will CoroutineScope(//Context) work with?

    Coroutine(Dispatchers.Default) //Default, Main, IO, Unconfined.

    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            //Context
            println("Starting")
            CoroutineScope(Dispatchers.Default).launch {
                delay(5000)
                println("coroutine Scope ...")
            }
            println("Ending")
    
        }
    }
    
    
    Output: 
    2022-09-11 15:51:19.962 10520-10520/me.king.androidApp I/System.out: Starting
    2022-09-11 15:51:20.003 10520-10520/me.king.androidApp I/System.out: Ending
    2022-09-11 15:51:25.009 10520-10627/me.king.androidApp I/System.out: coroutine Scope ...
    

    CoroutineScope is similar in structure to Global scope, but differs from it by not working in the whole application.

    CoroutineScope(Dispatcher.) is widely used.




    nested coroutines:

    
    fun main(){
    
        runBlocking {
            launch {
                delay(5000)
                println("run blocking...")
            }
    
            coroutineScope {
                launch {
                    delay(2500)
                    println("it is coroutine scope")
                }
            }
    
        }
    
    }
    
    
    Output: 
    it is coroutine scope
    run blocking...
    

    2.5 seconds coroutineScope worked then run blocking. Why didn't it block runblocking?

    -Because the bottom coroutineScope is already inside runBlocking. runBlocking blocks outside.




    Dispatchers:

    Dispatchers can be used together.

  • Default -> Cpu intensive. It is used for visual processing or when working with very large data.
  • Main -> It is used for UI related interface operations that the user will see.
  • IO -> input/output (used for networking operation) Extracting data from internet, getting data from database etc. like retrofit...
  • Unconfined -> Where it was created is inherited. It changes depending on where it is run.


  • 
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            runBlocking {
    
                launch(Dispatchers.Main) {
                    println("Main Thread: ${Thread.currentThread().name}")
                }
    
                launch(Dispatchers.IO) {
                    println("IO Thread: ${Thread.currentThread().name}")
                }
    
                launch(Dispatchers.Default) {
                    println("Default Thread: ${Thread.currentThread().name}")
                }
    
                launch(Dispatchers.Unconfined) {
                    println("Unconfined Thread: ${Thread.currentThread().name}")
                }
    
            }
    
        }
    }
    
    
    Output: 
    2022-09-11 17:09:56.286 12093-12156/me.king.androidApp I/System.out: IO Thread: DefaultDispatcher-worker-1
    2022-09-11 17:09:56.286 12093-12157/me.king.androidApp I/System.out: Default Thread: DefaultDispatcher-worker-2
    2022-09-11 17:09:56.286 12093-12093/me.king.androidApp I/System.out: Unconfined Thread: main
    

    -Unconfined already worked as Main. we're already in the main, that thread didn't even come up.




    SUSPEND FUN:

    Suspend functions are functions that can be run in a coroutine. These functions are suspendable, they can be stopped and run at any time.

    If a function is running a coroutine, it must be a suspend function. Also, if there is another function that we call this function, it should be suspend or if it is not suspend, it should be written into the coroutine scope. So we have 2 options.

    
    fun main(){
    
        runBlocking {
            delay(2000)
            println("run block working")
            myfun()
        }
          //myfun() //error! cause main is not suspend function. if we make "suspend fun main", it will work. OR we can use this inside CoroutineScope like above.
    }
    
    suspend fun myfun() {
        coroutineScope {
            delay(3000)
            println("suspend fun")
        }
    }
    
    
    Output: 
    run block working
    suspend fun
    



    async

    What happens when we use this instead of launch?

    Async actually expects a response. We will pull data from the internet, we do not know what and when we will pull it, so it is used for asynchronous processing.

    When we use launch, we can get an incorrect result as follows.

    
    fun main(){
    
        var userName = ""
        var userAge = 0
    
        runBlocking {
    
            launch {
                val downloadedName = downloadName()
                userName = downloadedName
            }
    
            launch {
                val downloadedAge = downloadAge()
                userAge = downloadedAge
            }
    
            println("$userName $userAge")
        }
    
    }
    
    suspend fun downloadName():String{
        delay(2000)
        val userName = "Ati"
        println("username download")
        return userName
    }
    
    suspend fun downloadAge():Int{
        delay(4000)
        val userAge = 23
        println("userAge Download")
        return userAge
    }
    
    
    Output: 
    0
    username download
    userAge Download
    

    Why is this happening? Because they work at different times. That's exactly why async should be used. We will use async instead of launch to sync.




    Async

    
    
    fun main(){
    
        var userName = ""
        var userAge = 0
    
        runBlocking {
            val downloadedName = async {
                downloadName()
            }
    
            val downloadAge = async {
                downloadAge()
            }
    
            userName = downloadedName.await()
            userAge = downloadAge.await()
    
            println(userName +"   "+ userAge)
    
        }
    
    }
    
    suspend fun downloadName():String{
        delay(2000)
        val userName = "Ati"
        println("username download")
        return userName
    }
    
    suspend fun downloadAge():Int{
        delay(4000)
        val userAge = 23
        println("userAge Download")
        return userAge
    }
    
    
    Output: 
    username download
    userAge Download
    Ati   23
    

    We made them wait with await() and keep them in sync. So we solved the problem by using async instead of suspend.




    Job

    We can equate launch to job and return job. We can control these returned jobs. Like canceling, starting...

    
    fun main(){
    
        runBlocking {
         val myJob = launch {
                delay(2000)
                println("job")
    
             val secondJob = launch {  //we can create a second job in any job
                 println("job2")
             }
            }
    
            myJob.invokeOnCompletion {  //myJob is complete. what we doing?
                println("job is complete")
            }
    
            //myJob.cancel()  if we cancel myJob, secondJob is not working cause, secondJob is inside myJob.
    
        }
    
    
    Output: 
    job
    job2
    job is complete
    



    withContext

    How do we switch from one dispatcher to another?

    With "withContext" we can operate in the same launch or in the same scope in different threads.

    We see this most often in examples where we start with Dispatchers.IO and end with Dispatchers.Main.

    
    fun main(){
    
        runBlocking { 
            
            launch(Dispatchers.Default) { 
                println("Context: $coroutineContext")
                withContext(Dispatchers.IO){
                    println("Context: $coroutineContext")
                }
            }
            
        }
    
    }
    
    
    Output: 
    Context: [StandaloneCoroutine{Active}@40905687, Dispatchers.Default]
    Context: [DispatchedCoroutine{Active}@69a36fb7, Dispatchers.IO]
    



    Exception handling

    In coroutines, the exceptions are caught with the CoroutineExceptionHandler, not with the try catch.

    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
           val handler = CoroutineExceptionHandler{ coroutineContext, throwable ->
           println("exception -> " + throwable.localizedMessage)
           }
    
            lifecycleScope.launch(handler) {
                throw Exception("fail!")
            }
    
        }
    }
    
    
    
    Output: 
    2022-09-12 17:45:40.639 1928-1928/me.king.androidApp I/System.out: exception -> fail!
    

    If you have more than one launch scope and one of them fails, the scopes below will not work and will be canceled. So supervisorScope is used here.



    without superviseorScope

    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
           val handler = CoroutineExceptionHandler{ coroutineContext, throwable ->
           println("exception -> " + throwable.localizedMessage)
           }
    
            lifecycleScope.launch(handler) {
             launch {
                 throw Exception("fail!")
             }
               launch {
                   delay(2000)
                   println("its working")
               }
            }
    
        }
    }
    
    
    Output: 
    2022-09-12 17:50:38.724 7868-7868/me.king.androidApp I/System.out: exception -> fail!
    
  • Note that: "its working" is not printed!


  • with supervisorScope

    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
           val handler = CoroutineExceptionHandler{ coroutineContext, throwable ->
           println("exception -> " + throwable.localizedMessage)
           }
    
            lifecycleScope.launch(handler) {
                supervisorScope {
                    launch {
                        throw Exception("fail!")
                    }
                    launch {
                        delay(2000)
                        println("its working")
                    }
                }
    
            }
    
        }
    }
    
    
    
    Output: 
    2022-09-12 17:53:28.288 8056-8056/me.king.androidApp I/System.out: exception -> fail!
    2022-09-12 17:53:30.291 8056-8056/me.king.androidApp I/System.out: its working
    
  • Note that: "its working" is printed!