https://kotlinlang.org logo
#coroutines
Title
# coroutines
k

Kirill Vasilenko

06/11/2021, 8:46 AM
Hi there! This message is for those who are interested in the opportunity to write their view models in a much simpler way and keep them at the multiplatform level. As you may know,
StateFlow
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.
Copy code
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
Copy code
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/2631
🙌 1
e

ephemient

06/11/2021, 9:57 AM
way too much crossposting
1
a

andylamax

06/11/2021, 10:45 AM
almost feels like a spam
j

jw

06/11/2021, 10:51 AM
definitely spam
3
k

Kirill Vasilenko

06/11/2021, 11:07 AM
Sorry for the inconvenience. I just wanted to reach all the audience that could get benefits from the topic and be interested in it. I will do my best to cut the numbers of targets channels as many as possible in the future. Thank you for your patience🙂
j

Joost Klitsie

06/11/2021, 2:53 PM
from you last message:
Copy code
The main idea is to avoid being suspended and couroutine scopes where it is possible. Making view models should be as simple as possible.
if you want to combine them automatically you will either have to collect the stateflows and create a new stateflow out of the result, which needs definitely a scope
otherwise you have to make a wrapper class that also stores the transformation, then if you get the
value
it will basically run the transformation
Copy code
fun <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)

}
Copy code
val canDoSomethingScary: StateFlow<Boolean> = combineStates(firstField, secondField) { first, second ->
    first.isNotBlank() && second > 0 && second < 10
}
should this really be in the stdlib?
d

Dominaezzz

06/11/2021, 3:28 PM
State flows have some runtime contracts that prevent you from using that
CombinedStateFlow
as is. One of which include "distinct until changed emissions". Just don't implement `StateFlow`and you're good to go.
👍 1
j

Joost Klitsie

06/11/2021, 3:30 PM
or just add a
Copy code
.distinctUntilChanged()
k

Kirill Vasilenko

06/12/2021, 1:56 PM
@Joost Klitsie , @Dominaezzz thank you for your answers and notes!
Copy code
otherwise 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.
Copy code
.distinctUntilChanged()
It is an absolutely reasonable and useful addition, thank you!
j

Joost Klitsie

06/12/2021, 2:00 PM
Any 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 option
I guess any library will mirror that
d

Dominaezzz

06/12/2021, 2:02 PM
There should be a compiler plugin for that. 🤔
j

Joost Klitsie

06/12/2021, 2:02 PM
Having 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
k

Kirill Vasilenko

06/12/2021, 2:06 PM
@Joost Klitsie
Copy code
should 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.
@Joost Klitsie
Copy code
Any number of combine means creating a vararg...
It is not necessary. You can take a look at another way here
@Dominaezzz
Copy code
There 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?
d

Dominaezzz

06/12/2021, 2:12 PM
Any 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 option
I was referring to this
j

Joost Klitsie

06/12/2021, 2:13 PM
But to write a compiler plugin for that? :)
k

Kirill Vasilenko

06/12/2021, 2:19 PM
@Joost Klitsie
Copy code
Having 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🙂
j

Joost Klitsie

06/14/2021, 6:49 AM
but then you will have a duplicate state and you cannot do this without having a coroutinescope involved, because you will need to collect the stateflows (for which you need a scope)
k

Kirill Vasilenko

06/14/2021, 12:07 PM
I see your point. It is actually possible to not have a
CoroutineScope.
For example, we can update the derived states synchronously when one of the parent states is changed, as described here
j

Joost Klitsie

06/14/2021, 8:30 PM
but such a transactionmanager will just live forever? I mean the whole point of using scopes is you don't have to subscribe/unsubscribe. Also, it sounds to me like if your derived states are inconsistent there is perhaps a problem with the code or pattern you are using. Also, the state should be consistent and SSOT. Copying a state into another state violates SSOT principle. But my main problem is that if you rely on side effects a transaction manager will not help you 🙂 You somehow want 3 states to be updated simultaneously, I cannot wrap my mind around that one to be honest. So set all values and suspend all collection during that, while hoping nobody will do a value read in the mean time. I think restructuring the code so you don't have the issue that your states can be in an inconsistent state, is a better solution.
6 Views