Вызов Kotlin Coroutines параллельно с функцией приостановки в Android

Я относительно новичок в сопрограммах, поэтому мне было интересно, как я могу решить свою небольшую локальную проблему без значительной реструктуризации моих кодов Android.

Вот простая настройка. Моя ViewModel вызывает функцию suspend из хранилища:

// ...ViewModel.kt

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

Это очень удобно, так как у меня есть viewModelScope и я вызываю функцию приостановки из моего репозитория. Мне все равно, как хранилище загружает данные, я просто приостанавливаю, пока они не будут возвращены мне.

Мой репозиторий данных делает несколько вызовов, используя 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 нетронутыми - это не должно волновать, как загружаются данные, просто запускает функцию приостановки из собственной области видимости. Я также не хочу передавать какие-либо области видимости или другие объекты в мой репозиторий.

Как я могу сделать это внутри функции приостановки? Присутствие там как-то неявно присутствует? async() возможен / разрешен / полезен?

Всего 2 ответа


Вы можете использовать async и awaitAll для этого. Вам нужна область сопрограмм для запуска новых сопрограмм, но вы можете создать такую, которая наследует существующий контекст, используя 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()

Да, чтобы выполнить несколько сопрограмм одновременно, вам нужно будет использовать async несколько раз, а затем вызывать await для всех Deferred возвращаемых из async вызовов.

Вы можете найти очень похожий пример здесь .

Это самая важная часть кода:

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
}

Есть идеи?

10000