Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wait for end of a coroutine

I have some code below. Delay (3000) is just replacement for a long loop (or cycle). I’m expecting that after completion of loop println(res) will print “Some String” and then enable button. But in real life println(res) prints an empty string and button became enabled at same time when I click it. My question is: how I can wait for end of a coroutine and only after completion of the coroutine run println(res) and button.isEnabled = true.

private var res: String = ""

private suspend fun test(): String {
    delay(3000) // delay - just replacement for long loop
    return "Some String" // String received after loop
}

fun onClick(view: View) {
    res = ""
    button.isEnabled = false
    GlobalScope.launch {
        res = withContext(Dispatchers.Default) {
            test()
        }
    }
    println(res) // 1. trying to get string received after loop, but not working
    button.isEnabled = true // 2. button must be enabled after loop in cycle, but it's not waiting till end of loop
}
like image 331
Shkum Avatar asked Dec 26 '19 17:12

Shkum


4 Answers

launch is for situations where you don't care about the result outside the coroutine. To retrieve the result of a coroutine use async.

val res = GlobalScope.async(Dispatchers.Default) { test() }.await()

Note: avoid using GlobalScope, provide your own CoroutineScope instead.

like image 142
m0skit0 Avatar answered Nov 08 '22 00:11

m0skit0


The main thing to understand here is that code within coroutine is by default executed sequentially. I.e. coroutine is executed asynchronously in relation to "sibling" code, but code within coroutine executes synchronously by default.

For example:

fun DoSometing () { 

coroutineA {
doSomethingA1()
doSomethingA2()
}

some additional code 

}

Corroutine A will execute async in relation to some additional code but doSometingA2 will be executed after doSomethingA1 is done.

That means, that within a coroutine every next piece of code will be executed after the previous one is done. So, whatever you want to execute when coroutine is done, you just put at the end of that coroutine and declare context (withContext) in which you want to execute it.

The exception is of course if you start another async piece of code within coroutine (like another coroutine).

EDIT: If you need to update UI from the coroutine, you should execute that on the main context, i.e. you'll have something like this:

GlobalScope.launch (Dispatchers.IO) {

   //do some background work
   ...
   withContext (Dispatchers.Main) { 
       //update the UI 
       button.isEnabled=true  
       ...
     }
}
like image 35
daneejela Avatar answered Nov 08 '22 02:11

daneejela


You can try some thing like this:

suspend fun saveInDb() {
    val value = GlobalScope.async {
       delay(1000)
       println("thread running on [${Thread.currentThread().name}]")
       10
    }
    println("value =  ${value.await()} thread running on [${Thread.currentThread().name}]")
} 

await will wait for the coroutine to finish and then run code below it

fun onClick(view: View) {
    res = ""
    button.isEnabled = false
    GlobalScope.launch(Dispatchers.Main){ // launches coroutine in main thread
         updateUi()
    }
}

suspend fun updateUi(){
    val value = GlobalScope.async { // creates worker thread
       res = withContext(Dispatchers.Default) {
          test()
       }
    }
    println(value.await()) //waits for workerthread to finish
    button.isEnabled = true //runs on ui thread as calling function is on Dispatchers.main
}
like image 13
Tushar Saha Avatar answered Nov 08 '22 01:11

Tushar Saha


why you don't move println and button.isEnabled inside GlobalScope.launch coroutine.

fun onClick(view: View) {
    res = ""
    button.isEnabled = false
    GlobalScope.launch {
        val res = withContext(Dispatchers.Default) {
            test()
        }

        println(res)
        button.isEnabled = true 
    }
}

if you whant your code run on main thread add Dispatchers.Main as an argument.

GlobalScope.launch(Dispatchers.Main) {
   val res = withContext(Dispatchers.Default) {
            test()
        }

        println(res)
        button.isEnabled = true 
}

now println and button.isEnabled run on main thread and test() fun runs on Default which in real is a worker thread.

like image 1
Mojtaba Haddadi Avatar answered Nov 08 '22 00:11

Mojtaba Haddadi