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