从Android中的暂停功能并行调用Kotlin协程

时间:2020-03-24 17:55:42

标签: android kotlin kotlin-coroutines

我对协程还比较陌生,所以我想知道如何在不大量重构Android代码的情况下解决本地的小问题。

这是一个简单的设置。我的ViewModel从存储库中调用suspend函数:

// ...ViewModel.kt

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

这非常方便,因为我有一个viewModelScope由Android为我准备,并且我从存储库中调用了suspend函数。我不在乎存储库如何加载数据,我只是暂停直到数据返回给我。

我的数据存储库使用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)
}

如您所见,由于loadData()是一个暂停函数,对retrofitApi.getSubItem(itemID)的所有调用将顺序执行。

但是,我想并行执行它们,就像协程中的async() / await()一样。

我想保持ViewModel代码不变-它不在乎数据是如何加载的,只是从自己的作用域中启动一个suspend函数。我也不想将任何范围或其他对象传递到我的存储库。

如何在暂停功能中执行此操作?范围在某种程度上隐式存在吗?打电话给async()是可能的/允许的/好的做法吗?

2 个答案:

答案 0 :(得分:1)

您可以为此使用asyncawaitAll。您需要一个协程范围来启动新的协程,但是您可以创建一个使用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 Pair(items, subItems)
}

您可能在原始代码中使用了flatten来简化它。 (只是指出这与分解这些并行任务无关。)它看起来像这样:

val subItems = itemIDs.map { itemID ->
        retrofitApi.getSubItems(itemID)
    }.flatten()

答案 1 :(得分:1)

是的,要同时执行多个协程,您将需要多次使用async启动器,然后在await调用返回的所有Deferred上调用async。 / p>

您可以找到一个非常相似的示例here

这是代码中最相关的部分:

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
}