简体   繁体   English

Kotlin 协程流程中的 RxJava .toList() 等价物

[英]Equivalent of RxJava .toList() in Kotlin coroutines flow

I have a situation where I need to observe userIds then use those userIds to observe users.我有一种情况,我需要观察 userIds 然后使用这些 userIds 观察用户。 Either userIds or users could change at any time and I want to keep the emitted users up to date. userIds 或 users 都可以随时更改,我想让发出的用户保持最新状态。 Here is an example of the sources of data I have:这是我拥有的数据来源的示例:


data class User(val name: String)

fun observeBestUserIds(): Flow<List<String>> {
    return flow {
        emit(listOf("abc", "def"))
        delay(500)
        emit(listOf("123", "234"))
    }
}

fun observeUserForId(userId: String): Flow<User> {
    return flow {
        emit(User("${userId}_name"))
        delay(2000)
        emit(User("${userId}_name_updated"))
    }
}

In this scenario I want the emissions to be:在这种情况下,我希望排放量是:

[User(abc_name), User(def_name)] , then [User(abc_name), User(def_name)] , 然后

[User(123_name), User(234_name)] , then [User(123_name), User(234_name)] , 然后

[User(123_name_updated), User(234_name_updated)]

I think I can achieve this in RxJava like this:我想我可以像这样在 RxJava 中实现这一点:

observeBestUserIds.concatMapSingle { ids ->
    Observable.fromIterable(ids)
        .concatMap { id ->
            observeUserForId(id)
        }
        .toList()
}

What function would I write to make a flow that emits that?我会写什么函数来制作一个发出它的流?

I believe you're looking for combine , which gives you an array that you can easily call toList() on:我相信您正在寻找combine ,它为您提供了一个可以轻松调用toList()的数组:

observeBestUserIds().collectLatest { ids ->
    combine(
        ids.map { id -> observeUserForId(id) }
    ) {
        it.toList()
    }.collect {
        println(it)
    } 
}

And here's the inner part with more explicit parameter names since you can't see the IDE's type hinting on Stack Overflow:这是具有更明确参数名称的内部部分,因为您在 Stack Overflow 上看不到 IDE 的类型提示:

combine(
    ids.map { id -> observeUserForId(id) }
) { arrayOfUsers: Array<User> ->
    arrayOfUsers.toList()
}.collect { listOfUsers: List<User> ->
    println(listOfUsers)
}

Output:输出:

[User(name=abc_name), User(name=def_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]

Live demo (note that in the demo, all the output appears at once, but this is a limitation of the demo site - the lines appear with the timing you'd expect when the code is run locally)现场演示(请注意,在演示中,所有输出都会一次出现,但这是演示站点的限制 - 当代码在本地运行时,这些行出现的时间与您期望的时间一致)

This avoids the ( abc_name_updated , def_name_updated ) discussed in the original question.这避免了原始问题中讨论的( abc_name_updateddef_name_updated )。 However, there's still an intermediate emission with 123_name_updated and 234_name because the 123_name_updated is emitted first and it sends the combined version immediately because they're the latest from each flow.然而,仍然有123_name_updated234_name的中间发射,因为123_name_updated首先发射,它立即发送组合版本,因为它们是每个流中的最新版本。

However, this can be avoided by debouncing the emissions (on my machine, a timeout as small as 1ms works, but I did 20ms to be conservative):但是,这可以通过对发射进行去抖动来避免(在我的机器上,只有 1ms 的超时有效,但为了保守起见,我做了 20ms):

observeBestUserIds().collectLatest { ids ->
    combine(
        ids.map { id -> observeUserForId(id) }
    ) {
        it.toList()
    }.debounce(timeoutMillis = 20).collect {
        println(it)
    }
}

which gets you the exact output you wanted:这让你得到你想要的确切输出:

[User(name=abc_name), User(name=def_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]

Live demo现场演示

This is unfortunatly non trivial with the current state of kotlin Flow, there seem to be important operators missing.不幸的是,这与 kotlin Flow 的当前状态无关,似乎缺少重要的运算符。 But please notice that you are not looking for rxJavas toList().但请注意,您不是在寻找 rxJavas toList()。 If you would try to to do it with toList and concatMap in rxjava you would have to wait till all observabes finish.如果您尝试在 rxjava 中使用toListconcatMap执行此toListconcatMap必须等到所有观察完成。 This is not what you want.这不是你想要的。

Unfortunately for you I think there is no way around a custom function.不幸的是,我认为没有办法绕过自定义函数。

It would have to aggregate all the results returned by observeUserForId for all the ids which you would pass to it.它必须为您将传递给它的所有 id 汇总由observeUserForId返回的所有结果。 It would also not be a simple windowing function, since in reality it is conceivable that one observeUserForId already returned twice and another call still didn't finish.它也不会是一个简单的窗口函数,因为实际上可以想象一个observeUserForId已经返回了两次而另一个调用仍未完成。 So checking whether you already have the same number of users as you passed ids into your aggregating functions isn't enought, you also have to group by user id.因此,检查您是否已经拥有与您将 id 传递到聚合函数中的用户数量相同是不够的,您还必须按用户 id 进行分组。

I'll try to add code later today.我将在今天晚些时候尝试添加代码。

Edit: As promised here is my solution I took the liberty of augmenting the requirements slightly.编辑:正如我所承诺的,这是我的解决方案,我冒昧地稍微增加了要求。 So the flow will emit every time all userIds have values and an underlying user changes.因此,每次所有 userId 都有值并且底层用户发生更改时,流程都会发出。 I think this is more likely what you want since users probably don't change properties in lockstep.我认为这更有可能是您想要的,因为用户可能不会在锁步中更改属性。

Nevertheless if this is not what you want leave a comment.不过,如果这不是您想要的,请发表评论。

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking


data class User(val name: String)

fun observeBestUserIds(): Flow<List<String>> {
    return flow {
        emit(listOf("abc", "def"))
        delay(500)
        emit(listOf("123", "234"))
    }
}

fun observeUserForId(userId: String): Flow<User> {
    return flow {
        emit(User("${userId}_name"))
        delay(2000)
        emit(User("${userId}_name_updated"))
    }
}

inline fun <reified K, V> buildMap(keys: Set<K>, crossinline valueFunc: (K) -> Flow<V>): Flow<Map<K, V>> = flow {
    val keysSize = keys.size
    val valuesMap = HashMap<K, V>(keys.size)
    flowOf(*keys.toTypedArray())
            .flatMapMerge { key -> valueFunc(key).map {v -> Pair(key, v)} }
            .collect { (key, value) ->
                valuesMap[key] = value
                if (valuesMap.keys.size == keysSize) {
                    emit(valuesMap.toMap())
                }
            }
}

fun observeUsersForIds(): Flow<List<User>> {
    return observeBestUserIds().flatMapLatest { ids -> buildMap(ids.toSet(), ::observeUserForId as (String) -> Flow<User>) }
            .map { m -> m.values.toList() }
}


fun main() = runBlocking {
    observeUsersForIds()
        .collect { user ->
            println(user)
        }
}

This will return这将返回

[User(name=def_name), User(name=abc_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]

You can run the code online here您可以在此处在线运行代码

You can use flatMapConcat您可以使用flatMapConcat

val users = observeBestUserIds()
        .flatMapConcat { ids ->
            flowOf(*ids.toTypedArray())
                .map { id ->
                    observeUserForId(id)
                }
        }
        .flattenConcat()
        .toList()

or或者

    observeBestUserIds()
        .flatMapConcat { ids ->
            flowOf(*ids.toTypedArray())
                .map { id ->
                    observeUserForId(id)
                }
        }
        .flattenConcat()
        .collect { user ->

        }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM