Kirill Vasilenko
06/11/2021, 8:46 AMStateFlow
is a really convenient thing for implementing reactive view models, but there is a big problem - when you combine them (with combine(...)
or map(...)
operators) they turn into Flow
that is not so convenient.
val firstField: StateFlow<String>
val secondField: StateFlow<Int>
val canDoSomethingScary: Flow<Boolean> = combine(firstField, secondField) { first, second ->
first.isNotBlank() && second > 0 && second < 10
}
// method has to be suspended :(
suspend fun doSomethingScary1() {
// had to have coroutineScope here :(
if (!canDoSomethingScary.stateIn(coroutineScope).value) return
// do something scary
}
There is a proposal on GitHub about having an opportunity to combine StateFlows
and get StateFlow
. There are also examples why getting Flow
is not convenient and working code snippets with the solution, using which one will be able to write much clearer code and keep their view models at the multiplatform level right now. Like this
val firstField: StateFlow<String>
val secondField: StateFlow<Int>
val canDoSomethingScary: StateFlow<Boolean> = combineStates(firstField, secondField) { first, second ->
first.isNotBlank() && second > 0 && second < 10
}
// method doesn't have to be suspended :)
fun doSomethingScary() {
if (!canDoSomethingScary.value) return
// do something scary
}
Please vote and discuss the proposal there to induce the Kotlin team to include it (or a better solution) in the kotlinx.coroutines
in the following releases.
https://github.com/Kotlin/kotlinx.coroutines/issues/2631ephemient
06/11/2021, 9:57 AMandylamax
06/11/2021, 10:45 AMjw
06/11/2021, 10:51 AMKirill Vasilenko
06/11/2021, 11:07 AMJoost Klitsie
06/11/2021, 2:53 PMThe main idea is to avoid being suspended and couroutine scopes where it is possible. Making view models should be as simple as possible.
value
it will basically run the transformationfun <T1, T2, R> combineStates(
state1: StateFlow<T1>,
state2: StateFlow<T2>,
transform: (T1, T2) -> R
) = CombinedStateFlow(state1, state2, transform)
class CombinedStateFlow<T1, T2, R> constructor(
private val state1: StateFlow<T1>,
private val state2: StateFlow<T2>,
private val transform: (T1, T2) -> R
) : StateFlow<R>, Flow<R> by state1.combine(state2, { arg1, arg2 -> transform(arg1, arg2) }).distinctUntilChanged() {
override val value: R
get() = transform(state1.value, state2.value)
override val replayCache: List<R>
get() = listOf(value)
}
val canDoSomethingScary: StateFlow<Boolean> = combineStates(firstField, secondField) { first, second ->
first.isNotBlank() && second > 0 && second < 10
}
Dominaezzz
06/11/2021, 3:28 PMCombinedStateFlow
as is. One of which include "distinct until changed emissions". Just don't implement `StateFlow`and you're good to go.Joost Klitsie
06/11/2021, 3:30 PM.distinctUntilChanged()
Kirill Vasilenko
06/12/2021, 1:56 PMotherwise you have to make a wrapper class that also stores the transformation, then if you get the value it will basically run the transformation
It is the exact idea that lay behind the issue, but it implemented there in a bit more scalable way:
• Your solution requires to have a separate class for each number of StateFlows
that one wants to combine.
• The solution in the issue requires to have a single class for any number of StateFlows
that one wants to combine.
.distinctUntilChanged()
It is an absolutely reasonable and useful addition, thank you!Joost Klitsie
06/12/2021, 2:00 PMDominaezzz
06/12/2021, 2:02 PMJoost Klitsie
06/12/2021, 2:02 PMKirill Vasilenko
06/12/2021, 2:06 PMshould this really be in the stdlib?
Exactly this one definitely should not, but at the same time, there apparently should be an opportunity to combine StateFlows
to StateFlow
without a not really elegant boilerplate code around it as we have now.
I don't know if you develop applications with UI or just server-side, but if someone developed an Android/iOS app and tried to keep code up to view models layer in the multiplatform module, they would heavily use such primitive.Any number of combine means creating a vararg...
It is not necessary. You can take a look at another way hereThere should be a compiler plugin for that.
I don't really understand why. Could you please elaborate a bit on it? Why do we have to have a compiler plugin for such a common and quite easy to implement thing?Dominaezzz
06/12/2021, 2:12 PMAny number of combine means creating a vararg just like the current combine methods, you have a bunch for 2, 3 , or 4 and then the vararg optionI was referring to this
Joost Klitsie
06/12/2021, 2:13 PMKirill Vasilenko
06/12/2021, 2:19 PMHaving a combined stateflow sounds anyway like a virtual engagement, you wouldn't want to store the state but get the state from the existing stateflows. So a wrapper class sounds pretty logical to me
Agree👍 So the point is to have it and appropriate functions in kotlinx.coroutines
And I would improve such a wrapper to not recalculate the whole tree on every read, but wait a minute, if we don't recalculate the whole tree on every read, we have to store the result somewhere🙂Joost Klitsie
06/14/2021, 6:49 AMKirill Vasilenko
06/14/2021, 12:07 PMCoroutineScope.
For example, we can update the derived states synchronously when one of the parent states is changed, as described hereJoost Klitsie
06/14/2021, 8:30 PM