Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parallel requests with coroutines

I'm trying to fetch some data from multiple locations to fill a recyclerView. I used to use callbacks, which worked fine, but need to refactor it to coroutines.

So i have a list of retrofit services and call each on of them parallerl. Then i can update the recyclerView with the onResponse callback. How can i achive this with coroutines.

I tried something like that, but the next call is fired after i got a response:

runblocking {
    for (service in services) {
        val response = async(Dispatchers.IO) {
            service.getResponseAsync()
        }
        adapter.updateRecyclerView(response.await())
    }
}

With another approach i had the problem that i was not able to get back on the main thread to update my ui as i was using launch and could not await the response:

runblocking {
    services.foreach {
        launch(Dispatcher.IO) {
            val response = it.getResponseAsync()
        }
        withContext(Dispatcher.Main) {
            adapter.updateRecyclerView(response)
        }
    }
}

I'm thankfull for every tip ;) cheers patrick

like image 668
trice Avatar asked May 03 '20 22:05

trice


2 Answers

Start coroutines with launch instead of runBlocking. The examples below assume you're launching from a context that uses Dispatchers.Main by default. If that's not the case, you could use launch(Dispatchers.Main) for these.

If you want to update your view every time any of the parallel actions returns, then move your UI update inside the coroutines that you're launching for each of the service items:

for (service in services) {
    launch {
        val response = withContext(Dispatchers.IO) { service.getResponseAsync() }
        adapter.updateRecyclerView(response)
    }
}

If you only need to update once all of them have returned, you can use awaitAll. Here, your updateRecyclerView function would have to be written to handle a list of responses instead of one at a time.

launch {
    val responses = services.map { service ->
        async(Dispatchers.IO) { service.getResponseAsync() }
    }
    adapter.updateRecyclerView(responses.awaitAll())
}
like image 176
Tenfour04 Avatar answered Oct 20 '22 23:10

Tenfour04


The await() call suspends the current coroutine and frees the current thread for being attached by other queued coroutines.

So when await() is called the current coroutine suspends till the response is received, and that's why for loop does not complete (goes to next iteration before completion of before request).


First and foremost you should not be using the runBlocking here, it is highly discouraged to be used in production evironment.

You should instead be using the ViewModel scope provided by android for structured concurrency (cancels the request if no longer needed like if lifecycle of activity is over).

You can use view model scope like this in activity or fragment viewModelOwner.viewModelScope.launch(/*Other dispatcher if needed*/) {} or make a coroutine scope yourself with a job attached which cancels itself on onDestroy.


For the problem the coroutine does not do parallel requests, you can launch multiple request without await (ing) on them inside the for loop.

And select them, using select expression https://kotlinlang.org/docs/reference/coroutines/select-expression.html#selecting-deferred-values

Example:

viewModelOwner.viewModelScope.launch {
    val responses = mutableListOf<Deferred<TypeReturnedFromGetResponse>>()
    for (service in services) {
        async(Dispatchers.IO) {
            service.getResponseAsync()
        }.let(responses::add)
    }

    // adds which ever request is done first in oppose to awaiting for all then update
    for (i in responses.indices) {
        select<Unit> {
            for (response in responses) {
                response.onAwait {
                    adapter.updateRecyclerView(it)
                }
            }
        }
    }
}

PS: Using this method looks ugly but will update the adapter as soon as whichever request is first resolved, instead of awaiting for each and every request and then updating the items in it.

like image 27
Animesh Sahu Avatar answered Oct 20 '22 22:10

Animesh Sahu