Could you show some real-life examples with LiveData. I just don’t understand the example above because you use StateFlow.value, which is not how you want to use it in real life
I believe they didn’t mean “Observable” as particular class, but just any implementation of observable as a design pattern which suits your use case.
Also you can write composable adapter for any observable implementation.
But I still not quite sure what you try to implement, because in case of Composable you don’t need (and you should never rely at all) runBlocking and .value, because composable observes data using Flow adapter to convert it to composable sate, so no need to use value
Looks like we need to change runBlocking
to something more optimized that Composable
uses.
Well, for this you need a test that uses Composable, you can for example write test with ComposeRunner Android test and use collectAsState
runBlocking + stateFlow.value is not what Composable is doing with stateFlow , it’s very different because composable actually uses stateFlow.collect() and uses stateFlow.value only for initial value, so no blocking or relying on value except for initial valyue
So, the question is: how to make this stateFlow.collect()
within 1 line properly then? Extension is fine too.
stateFlow.collectAsState()
I didn’t find: how can it be used outside of the composable in a plain class?
You cannot, this is why I suggested above to use Android UI tests instead
The problem with emulating compose with non-composable test (even with proper reactive code) is that compose its own, particular semantics for recomposition, so if you target Compose, use a real Compose test, not try to emulate it with stateFlow.value, because code on Compose and code with pure reactive streams for this would be pretty different
Why I should write my entire program in tests? It doesn’t work this way, I need a regular code solution. You missed something.
Probably I misunderstood you.
Your original code uses test to emulate this. Do not use test then, use regular Compose code
We have
Compose
that can observeStateFlow
without extra coroutines
Isn’t you said that you use compose. why now you want to use in a plain class?
Anyway, if you want, you don’t need collectAsState, you need pure reactive solution
Because it’s a bad practice to use calculations in View. At least, you need to use viewModel.
Yes, calculations are in the model and the view observes the model for changes:
Model:
val up = 10
val bl = MutableStateFlow(3)
val bp = bl.map { it * 0.2f }
val ut = bp.map { (it * up).toInt() }
View:
ut.collectAsState(null)
This is really as simple as that. I still have problems following you and why do we need to complicate this with runBlocking()
, Flowable
wrappers and stuff.
Maybe the only tricky part here is that bl
have immediate value and transformations are also immediate, so we could expect we should have immediate value for ut
as well, but we don’t. This may be indeed counter-intuitive. We could either use null
as above for an initial value (it should be immediately replaced with the proper value) or we could extract ut
calculation to a separate function and call it here directly to get initial value.
In the past I planned to create a library for Kotlin and Java that provides “reactive variables”, with Kotlin sweetness and adapters for flows and other reactive solutions. Then I realized it wouldn’t be that useful, because people already use flows and RxJava and even if my library would help with some use cases, that would be probably not enough to justify using an entirely new tool. I stopped working on this.
Maybe I miss some use scenario that would make such a tool useful and you have this exactly scenario. But frankly, I still have problems following you and your case.
Because it’s a bad practice to use calculations in View
This is not what I propose, your reactive Flows (and mapping code) must be on side of VM/any other business abstraction and consumption of final result must be on side of composable with collectAsState or just simple flow.collect{}, @broot explained it really well.
My point was about the usage of collectAsState which is not the same as runBlocking and stateFlow.value
So, I tried another wrapper and it shown even worse performance.
class Flowable2<T>(val flow: Flow<T>, initial: T) {
var value: T = initial
private set
private val coroutineScope = CoroutineScope(Dispatchers.Default)
init {
coroutineScope.launch {
flow.collect {
value = it
}
}
}
}
The best solution as for now I have is:
val <T> Flow<T>.value: T get() = runBlocking { this@value.first() }
@Test
fun performanceBlocking(){
val counts = 100000
val fl = MutableStateFlow(2)
val a = fl.map { it }
var b = 0
val timeElapsed = measureTimeMillis {
repeat(counts) { b += a.value }
}
println("$timeElapsed, $b")
b = 0
val timeElapsed2 = measureTimeMillis {
repeat(counts) { b += fl.value }
}
println("$timeElapsed2, $b")
assertTrue { timeElapsed <= timeElapsed2 }
}
Shows 900 ms vs 1 ms
I just need to change runBlocking
to something synchronous but not sure what.