MaaAn13
MaaAn13

Reputation: 258

Suspending coroutine function till it receives result and then continue

I'm slowly learning kotlin-coroutines and want I wanted to start with was simple case where I iterate through list and each time call an API call, wait for response, do with it and then repeat the process every iteration.

viewModelScope.launch {
     list.forEach { item ->
          Log.d("TESTING", "forEach iteration before calling test(): ${item.id}")

          test()

          Log.d("TESTING", "forEach iteration after calling test(): ${item.id}")
     }
}

suspend fun test(item.id: String) = coroutineScope {
    Log.d("TESTING", "suspend test before receiving response: ${item.id}")

    val apiRepsonse = async {
        repository.doingApiCall() {
            Log.d("TESTING", "response: ${item.id}")
        }
    }.await()

    Log.d("TESTING", "suspend test after receiving response: ${item.id}")
}

So, if we imagine, that list contains: { 10, 15, 25, 35 } (id's)

I'd want to receive in logcat following:

Log.d("TESTING", "forEach iteration before calling test(): 10")
Log.d("TESTING", "suspend test before receiving response: 10")
Log.d("TESTING", "response: 10")
Log.d("TESTING", "suspend test after receiving response: 10")
Log.d("TESTING", "forEach iteration after calling test(): 10")
___________________
Log.d("TESTING", "forEach iteration before calling test(): 15")
Log.d("TESTING", "suspend test before receiving response: 15")
Log.d("TESTING", "response: 15")
Log.d("TESTING", "suspend test after receiving response: 15")
Log.d("TESTING", "forEach iteration after calling test(): 15")
...

But, right now I'm getting something like this:

Log.d("TESTING", "forEach iteration before calling test(): 10")
Log.d("TESTING", "suspend test before receiving response: 10")
Log.d("TESTING", "suspend test after receiving response: 10")
Log.d("TESTING", "forEach iteration after calling test(): 10")
Log.d("TESTING", "forEach iteration before calling test(): 15")
Log.d("TESTING", "suspend test before receiving response: 15")
Log.d("TESTING", "suspend test after receiving response: 15")
Log.d("TESTING", "forEach iteration after calling test(): 15")

Log.d("TESTING", "response: 10")
Log.d("TESTING", "response: 15")

...

Basically, for-each loop moves and does not wait for the API async response. I thought .await() is intended for this scenerio, but I might be missing something here as it clearly does not work as I imagined it.

Upvotes: 1

Views: 1471

Answers (3)

Tenfour04
Tenfour04

Reputation: 93561

Most likely repository.doingApiCall() is not a proper suspend function and is firing off some asynchronous action without suspending the coroutine. It looks like it takes a lambda as a callback, another clue that it is not a suspending function.

And if it were a suspending function, you wouldn't need to dispatch it with async.

As an aside, using async and immediately calling await on it is a more convoluted and less optimized way of using withContext(Dispatchers.Default) and it also swallows exceptions silently.

Upvotes: 2

Jemshit
Jemshit

Reputation: 10038

Did not test, this should work:

// same code here
viewModelScope.launch {
     list.forEach { item ->
          Log.d("TESTING", "forEach iteration before calling test(): ${item.id}")

          test()

          Log.d("TESTING", "forEach iteration after calling test(): ${item.id}")
     }
}

suspend fun test(item.id: String) = withContext(Dispatchers.IO) {
    Log.d("TESTING", "suspend test before receiving response: ${item.id}")

    repository.doingApiCall() {
        Log.d("TESTING", "response: ${item.id}")
    }

    Log.d("TESTING", "suspend test after receiving response: ${item.id}")
}

Two things changed:

  • instead of starting new coroutineScope, use withContext() so it suspends the caller
  • inside withContext(), you don't need to use async.await

Upvotes: 0

Blundell
Blundell

Reputation: 76458

Change

val apiRepsonse = async {
        repository.doingApiCall() {
            Log.d("TESTING", "response: ${item.id}")
        }
    }.await()

to

val apiRepsonse = async {
        Log.d("TESTING", "Start work in another 'thread'.")
        repository.doingApiCall() {
            Log.d("TESTING", "Finish work in another 'thread'.")
        }
    }.await()
Log.d("TESTING", "response: ${apiRepsonse.id}")

That way the value that your await is delivering is actually used.

I think it'll clear up your LogCat confusion.


I was trying to work through your problem myself, I have written it up as a test that can be run to be understood:

class TestFoo {

    @Test
    fun foo() {
        val job = GlobalScope.launch {
            listOf("10", "15").forEach { item ->
                println("TESTING forEach iteration before calling test(): ${item}")

                test(item)

                println("TESTING forEach iteration after calling test(): ${item}")
            }
        }
        while(job.isActive) {
            // wait
        }
    }

    private suspend fun test(item: String) = coroutineScope {
        println("TESTING suspend test before receiving response: ${item}")

        val apiRepsonse = async {
            delay(1000)
            println("TESTING response: ${item}")
            "Hello"
        }.await()

        println("TESTING suspend test after receiving response: ${item}")
    }

}

And here is the "LogCat":

enter image description here

Are you getting different logs because of some threading that is happening inside of repository.doingApiCall()?

Upvotes: 2

Related Questions