7

I'm relatively new to coroutines, so I was wondering how can I solve my small local problem without much restructuring my Android codes.

Here is a simple setup. My ViewModel calls a suspend function from repository:

// ...ViewModel.kt

fun loadData() {
    viewModelScope.launch {
        val data = dataRepository.loadData()
    }
}

This is quite convenient, since I have a viewModelScope prepared for me by Android and I call a suspend function from my Repository. I don't care how the repository loads the data, I just suspend till it is returned to me.

My data repository makes several calls using Retrofit:

//...DataRepository.kt

@MainThread
suspend fun loadData(): ... {
    // Retrofit switches the contexts for me, just
    // calling `suspend fun getItems()` here.
    val items = retrofitApi.getItems()
    val itemIDs = items.map { it.id }

    // Next, getting overall list of subItems for each item. Again, each call and context
    // switch for `suspend fun retrofitApi.getSubItems(itemID)` is handled by Retrofit.
    val subItems = itemIDs.fold(mutableListOf()) { result, itemID ->
        result.apply {
            addAll(retrofitApi.getSubItems(itemID)) // <- sequential :(
        }
    }

    return Pair(items, subItems)
}

As you can see, since loadData() is a suspend function, all the calls to retrofitApi.getSubItem(itemID) will be executed sequentially.

However, I would like to execute them in parallel, something like async() / await() in coroutines would do.

I want to keep the ViewModel codes untouched - it should not care how the data is loaded, just launches a suspend function from own scope. I also don't want to pass any kind of scopes or other objects to my repository.

How can I do this inside a suspend function? Is the scope somehow implicitly present there? Is calling async() possible/allowed/good practice?

frangulyan
  • 3,580
  • 4
  • 36
  • 64
  • What I have understood is you want these 2 network call in loadData() to run in parallel not sequential, am I right? But I see that you have dependant val val items = retrofitApi.getItems() val itemIDs = items.map { it.id } You are using itemIDs in : retrofitApi.getSubItems(itemID) So I think you should run them sequentially because of dependency. Please let me know if I have something missing – MustafaKhaled Mar 24 '20 at 18:04
  • 1
    It will be cleaner if you use `Flow` and the `flatMapMerge` operator. Some details [here](https://stackoverflow.com/questions/58658630/parallel-request-with-retrofit-coroutines-and-suspend-functions/58659423#58659423). – Marko Topolnik Mar 26 '20 at 10:46
  • 1
    Thanks @MarkoTopolnik ! I actually skipped `Flow` part when I was reading Coroutines as it seemed too much for me since I was completely new to this topic. Maybe now it is a good time to jump into it... :) – frangulyan Mar 27 '20 at 00:21
  • And it's a shame I didn't find that exact same question here on SO. Need to improve my googling skills. – frangulyan Mar 27 '20 at 00:25

2 Answers2

10

You can use async and awaitAll for this. You need a coroutine scope to launch new coroutines, but you can create one that inherits the existing context using coroutineScope.

suspend fun loadData(): Pair = coroutineScope {
    val items = retrofitApi.getItems()
    val itemIDs = items.map { it.id }
    val subItems = itemIDs.map { itemID ->
            async { retrofitApi.getSubItems(itemID) }
        }.awaitAll()
        .flatten()

    return@coroutineScope Pair(items, subItems)
}

You could have used flatten in your original code to simplify it a bit. (Just pointing out it's not related to decomposing these parallel tasks.) It would have looked like this:

val subItems = itemIDs.map { itemID ->
        retrofitApi.getSubItems(itemID)
    }.flatten()
Tomasz Dzieniak
  • 2,765
  • 3
  • 24
  • 42
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thanks! I'm actually not only new to coroutines, but also to Kotlin, good to learn new stuff :) – frangulyan Mar 24 '20 at 19:39
  • 1
    But it seems like `async` is not awailable inside my suspend function... – frangulyan Mar 24 '20 at 19:50
  • 1
    Sorry, see the edit. You need a scope for new coroutines, even if they're children of another one. You can easily create one using `coroutineScope`. – Tenfour04 Mar 24 '20 at 19:59
1

Yes, to execute several coroutines concurrently, you'll need to use async launcher multiple times, then call await on all Deferred returned from async calls.

You can find a very similar example here.

That's the most relevant part of the code:

private suspend fun computePartialProducts(computationRanges: Array<ComputationRange>) : List<BigInteger> = coroutineScope {
    return@coroutineScope withContext(Dispatchers.IO) {
        return@withContext computationRanges.map {
            computeProductForRangeAsync(it)
        }.awaitAll()
    }
}

private fun CoroutineScope.computeProductForRangeAsync(computationRange: ComputationRange) : Deferred<BigInteger> = async(Dispatchers.IO) {
    val rangeStart = computationRange.start
    val rangeEnd = computationRange.end

    var product = BigInteger("1")
    for (num in rangeStart..rangeEnd) {
        if (!isActive) {
            break
        }
        product = product.multiply(BigInteger(num.toString()))
    }

    return@async product
}
Vasiliy
  • 16,221
  • 11
  • 71
  • 127