Combine two state flows into new state flow

Viewed 6524

I have two state flows. Is it possible to combine them and get new state flow? Logically it should be possible because both state flows have initials values, but as I see combine function returns just Flow and not StateFlow.

6 Answers

You could use the combine operator, and then use the stateIn function for the Flow that results from that.

From the stateIn documentation in the kotlinx coroutines repository:

The stateIn function "Converts a cold Flow into a hot StateFlow that is started in the given coroutine scope, sharing the most recently emitted value from a single running instance of the upstream flow with multiple downstream subscribers."

It's signature, as of the time of writing this, is:

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

So you should be able to do whatever transformations you need to your Flows, including combining them, and then ultimately use stateIn to convert them back to a StateFlow.

It could look something like this (perhaps creating a Scrabble game point calculator):

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 would be of type StateFlow<String>, and you have successfully combined the two other StateFlows into one StateFlow.

So far I created function:

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))

Similar solution to @Nikola Despotoski, but in a form of an extension function

/**
 * 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))

Use combine operator, it takes two flows and a transformation function to combine the results from both flows.

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

Combine n state flows

@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())
)

Using:

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

Above mentioned solutions are using stateIn() with GlobalScope and policy as Eagerly which means these StateFlows will never stop being observed once created which can lead to the issues.

I already have mentioned details in this blog. Instead, create a separate class which derives a new 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

After this, you can implement it in your use case. For example:

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)
}

This approach is safe than other mentioned approaches since it'll only listen to the StateFlow updates when actually it's being collected (inside consumer's Coroutine scope)

Related