Martin
Martin

Reputation: 2904

How to make synchronous API call in coroutines?

I need to call API in for loop for each element, but it has to wait for each API call to return values, because if it will be asynchronous, order of returned elements could be mixed because each of those requests are asynchronous calls.

I want to wait inside for loop till value is returned. I used await() for this but it is not working as expected. Results are returned after for loop.

Code:

parts.forEach {p->
            if (p.listEmpty){
                validItems.add(PartItem(TYPE_WITHOUT_LIST, p))
            } else {
                launch {
                    val listReturned = getItemsForPart(p)
                    validItems.add(PartItem(TYPE_WITH_LIST, p, listReturned))
                }
            }
}

private val scope = CoroutineScope(Dispatchers.IO)
private suspend fun getItemsForPart(p: InnerItemPart): List<InnerPart>{
        val partResp = CompletableDeferred<List<InnerPart>>()
        scope.launch{
            val parent = api.getParentOfPart(p.partId)
            var filteredParts = emptyList<InnerPart>()
            if (parent != null){
                val startIndex = parent.innerPart.indexOfFirst { it.partId == p.startId }
                val endIndex = parent.innerPart.indexOfFirst { it.partId == p.endId }
                filteredParts = parent.subList(startIndex, endIndex).toMutableList()
                if (filteredParts.isNotEmpty()) filteredPart.removeAt(0)
            }
            partResp.complete(filteredParts)
        }

        return partResp.await()
}

Upvotes: 1

Views: 4199

Answers (3)

Tenfour04
Tenfour04

Reputation: 93531

Maybe you have reasons for them that I don't see, but the scope just for this suspend function looks unnecessary since you aren't using it for encapsulating the task for cancellation. And the CompletableDeferred is an unnecessary extra layer of complexity. Use withContext for the simplest way to do some background work and return it when done to a suspend function.

private suspend fun getItemsForPart(p: InnerItemPart): List<InnerPart> =
    withContext(Dispatchers.IO) {
        val parent = api.getParentOfPart(p.partId)
        var filteredParts = emptyList<InnerPart>()
        if (parent != null){
            val startIndex = parent.innerPart.indexOfFirst { it.partId == p.startId }
            val endIndex = parent.innerPart.indexOfFirst { it.partId == p.endId }
            filteredParts = parent.subList(startIndex, endIndex).toMutableList()
            if (filteredParts.isNotEmpty()) filteredPart.removeAt(0)
        }
        filteredParts
    }

And to further simplify the code:

private suspend fun getItemsForPart(p: InnerItemPart): List<InnerPart> =
    withContext(Dispatchers.IO) {
        api.getParentOfPart(p.partId)?.let { parent ->
            val startIndex = parent.innerPart.indexOfFirst { it.partId == p.startId }
            val endIndex = parent.innerPart.indexOfFirst { it.partId == p.endId }
            parent.subList(startIndex, endIndex).drop(1)
        }.orEmpty()
    }

Upvotes: 0

Sergio
Sergio

Reputation: 30595

You can launch a coroutine with Dispatchers.Main in function where you go though each list item and use withContext(Dispatchers.IO) in getItemsForPart() function. withContext(Dispatchers.IO) will change the context of the function to background thread:

fun applyForParts() = launch(Dispatchers.Main) {
    parts.forEach { p->
            if (p.listEmpty) {
                validItems.add(PartItem(TYPE_WITHOUT_LIST, p))
            } else {
                val listReturned = getItemsForPart(p) // call blocks for each item
                validItems.add(PartItem(TYPE_WITH_LIST, p, listReturned))
            }
    }
}

private suspend fun getItemsForPart(p: InnerItemPart): List<InnerPart> = withContext(Dispatchers.IO) {
    val parent = api.getParentOfPart(p.partId)
    var filteredParts = emptyList<InnerPart>()
    if (parent != null) {
        val startIndex = parent.innerPart.indexOfFirst { it.partId == p.startId }
        val endIndex = parent.innerPart.indexOfFirst { it.partId == p.endId }
        filteredParts = parent.subList(startIndex, endIndex).toMutableList()
        if (filteredParts.isNotEmpty()) filteredPart.removeAt(0)
    }

    return@withContext filteredParts
}

Upvotes: 0

Animesh Sahu
Animesh Sahu

Reputation: 8096

When you use launch inside forEach you say to launch a coroutine and queue it to the context and let the next code run. So loop does not suspend (goes to for next iteration and so on).

If you look at the definition, forEach is an inline function, which means it is going to be inlined at the call-site upon compilation so you can use suspending function directly there.

parts.forEach {p->
    if (p.listEmpty){
        validItems.add(PartItem(TYPE_WITHOUT_LIST, p))
    } else {
        val listReturned = getItemsForPart(p)  // suspends the current coroutine
        validItems.add(PartItem(TYPE_WITH_LIST, p, listReturned))
    }
}

You can use withContext for returning items it is lightweight and well optimized (unless you want to monitor child task, you don't need to create CoroutineScope).

private suspend fun getItemsForPart(p: InnerItemPart): List<InnerPart>{
    return withContext(Dispatchers.IO) {
        // your old code inside the launch, just put filteredParts in last line
        filteredParts
    }
}

Upvotes: 4

Related Questions