MutableStateFlow observe and change value from anywhere without creating a coroutine

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

1 Like

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

1 Like

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 observe StateFlow 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

1 Like

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.

1 Like

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.

2 Likes

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.