kotlin 将两个状态流合并为新的状态流

vddsk6oq  于 2023-03-30  发布在  Kotlin
关注(0)|答案(7)|浏览(231)

我有两个状态流。是否可以将它们组合并获得新的状态流?逻辑上应该是可能的,因为两个状态流都有初始值,但正如我所看到的,组合函数只返回Flow而不是StateFlow。

tktrz96b

tktrz96b1#

您可以使用combine运算符,然后对由此产生的Flow使用stateIn函数。
从kotlinx协程存储库中的stateIndocumentation
stateIn函数“将 coldFlow转换为在给定协程作用域中启动的 hotStateFlow,并与多个下游订阅者共享上游流的单个运行示例中最近发出的值。”
它的签名,截至写这篇文章的时候,是:

fun <T> Flow<T>.stateIn(
  scope: CoroutineScope,
  started: SharingStarted,
  initialValue: T
): StateFlow<T> (source)

因此,您应该能够对Flow进行所需的任何转换,包括组合它们,然后最终使用stateIn将它们转换回StateFlow
它可能看起来像这样(也许创建一个Scrabble游戏点数计算器):

val wordFlow = MutableStateFlow("Hi")
val pointFlow = MutableStateFlow(5)

val stateString = wordFlow.combine(pointFlow) { word, points ->
    "$word is worth $points points"
}.stateIn(viewModelScope, SharingStarted.Eagerly, "Default is worth 0 points")

stateString的类型为StateFlow<String>,而您已经成功地将另外两个StateFlows合并为一个StateFlow

3df52oht

3df52oht2#

到目前为止,我创建了函数:

fun <T1, T2, R> combineState(
        flow1: StateFlow<T1>,
        flow2: StateFlow<T2>,
        scope: CoroutineScope = GlobalScope,
        sharingStarted: SharingStarted = SharingStarted.Eagerly,
        transform: (T1, T2) -> R
): StateFlow<R> = combine(flow1, flow2) {
    o1, o2 -> transform.invoke(o1, o2)
}.stateIn(scope, sharingStarted, transform.invoke(flow1.value, flow2.value))
2exbekwf

2exbekwf3#

上述解决方案使用stateIn()GlobalScope,策略为Eagerly,这意味着***这些StateFlows一旦创建就永远不会停止观察,这可能导致问题***。
我已经提到了details in this blog。相反,创建一个单独的类来派生一个新的StateFlow

private class TransformedStateFlow<T>(
    private val getValue: () -> T,
    private val flow: Flow<T>
) : StateFlow<T> {
    
    override val replayCache: List<T> get() = listOf(value)
    override val value: T get() = getValue()

    override suspend fun collect(collector: FlowCollector<T>): Nothing =
        coroutineScope { flow.stateIn(this).collect(collector) }
}

/**
 * Returns [StateFlow] from [flow] having initial value from calculation of [getValue]
 */
fun <T> stateFlow(
    getValue: () -> T,
    flow: Flow<T>
): StateFlow<T> = TransformedStateFlow(getValue, flow)

/**
 * Combines all [stateFlows] and transforms them into another [StateFlow] with [transform]
 */
inline fun <reified T, R> combineStates(
    vararg stateFlows: StateFlow<T>,
    crossinline transform: (Array<T>) -> R
): StateFlow<R> = stateFlow(
    getValue = { transform(stateFlows.map { it.value }.toTypedArray()) },
    flow = combine(*stateFlows) { transform(it) }
)

/**
 * Variant of [combineStates] for combining 3 state flows
 */
inline fun <reified T1, reified T2, reified T3, R> combineStates(
    flow1: StateFlow<T1>,
    flow2: StateFlow<T2>,
    flow3: StateFlow<T3>,
    crossinline transform: (T1, T2, T3) -> R
) = combineStates(flow1, flow2, flow3) { (t1, t2, t3) ->
    transform(
        t1 as T1,
        t2 as T2,
        t3 as T3
    )
}

// Other variants for combining N StateFlows

在此之后,您可以在您的用例中实现它。例如:

private val isLoading = MutableStateFlow(false)
private val loggedInUser = MutableStateFlow<User?>(null)
private val error = MutableStateFlow<String?>(null)

// Combining these states to form a LoginState
val state: StateFlow<LoginState> = combineStates(isLoading, loggedInUser, error) { loading, user, errorMessage ->
  LoginState(loading, user, errorMessage)
}

这种方法比其他提到的方法更安全,因为它只会在实际收集StateFlow更新时(在消费者的Coroutine范围内)侦听它

xuo3flqw

xuo3flqw4#

与@Nikola Despotoski类似的解决方案,但采用扩展函数的形式

/**
 * Combines two [StateFlow]s into a single [StateFlow]
 */
fun <T1, T2, R> StateFlow<T1>.combineState(
  flow2: StateFlow<T2>,
  scope: CoroutineScope = GlobalScope,
  sharingStarted: SharingStarted = SharingStarted.Eagerly,
  transform: (T1, T2) -> R
): StateFlow<R> = combine(this, flow2) { o1, o2 -> transform.invoke(o1, o2) }
  .stateIn(scope, sharingStarted, transform.invoke(this.value, flow2.value))
xxslljrj

xxslljrj5#

合并n个状态流

@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
inline fun <reified T, R> combineStateFlow(
    vararg flows: StateFlow<T>,
    scope: CoroutineScope = GlobalScope,
    sharingStarted: SharingStarted = SharingStarted.Eagerly,
    crossinline transform: (Array<T>) -> R
): StateFlow<R> = combine(flows = flows) {
    transform.invoke(it)
}.stateIn(
    scope = scope,
    started = sharingStarted,
    initialValue = transform.invoke(flows.map {
        it.value
    }.toTypedArray())
)

使用方法:

data class A(val a: String)
data class B(val b: Int)

private val test1 = MutableStateFlow(A("a"))
private val test2 = MutableStateFlow(B(2))
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
private val _isValidForm = combineStateFlow(
    flows = arrayOf(test1, test2),
    scope = viewModelScope
) { combinedFlows: Array<Any> ->
    combinedFlows.map {
        val doSomething = when (it) {
            is A -> true
            is B -> false
            else -> false
        }
    }
}

Gist

bsxbgnwa

bsxbgnwa6#

如果你有一个初始值,那么合并两个StateFlows就不需要CoroutineScope

fun <T1, T2, R> combine(flow: StateFlow<T1>, flow2: StateFlow<T2>, initialValue: R, transform: suspend (a: T1, b: T2) -> R): StateFlow<R> {
    return object : StateFlow<R> {
        private val mutex = Mutex()
        override var value: R = initialValue

        override val replayCache: List<R> get() = listOf(value)

        override suspend fun collect(collector: FlowCollector<R>): Nothing {
            combine(flow, flow2, transform)
                .onStart { emit(initialValue) }
                .collect {
                    mutex.withLock {
                        value = it
                        collector.emit(it)
                    }
                }
            error("This exception is needed to 'return' Nothing. It won't be thrown (collection of StateFlow will never end)")
        }
    }
}

使用方法:

val letters = MutableStateFlow("A")
val numbers = MutableStateFlow(0)
val lettersAndNumbers: StateFlow<String> = combine(letters, numbers, "initial") { a, b -> a + b }

您可以使用此kotlin playground demo检查使用情况

suzh9iv8

suzh9iv87#

使用combine操作符,它需要两个流和一个转换函数来合并来自两个流的结果。

val int = MutableStateFlow(2)
 val double = MutableStateFlow(1.8)
 int.combine(double){ i, d ->
            i + d             
 }.collect(::println)

相关问题