MutableStateFlow observe and change value from anywhere without creating a coroutine

This topic is related to this one.

Android’s LiveData can be observed and changed anywhere without a coroutine with its observeForever() . How to do the same with MutableStateFlow that became a modern alternative to it?

Currently I made a workable work-around but it has a cascade design and boilerplate code.

    @Test
    fun mutableStateFlow() {
        val bl = MutableStateFlow(3)
        val bp = bl.map { it * 0.2f }
        fun getBp() = runBlocking { return@runBlocking bp.stateIn(CoroutineScope(Dispatchers.Default)) }

        val ut = bp.map { (it * up).toInt() }
        fun getUt() = runBlocking { //ReadonlyStateFlow is a private class
            return@runBlocking ut.stateIn(CoroutineScope(Dispatchers.Default))
        }

        assertEquals(6, getUt().value)

        bl.value = 4
        assertEquals(8, getUt().value)
    }

MutableStateFlow is only the first one variable here and stateIn provides only ReadonlyStateFlow that is a private class and can’t be transformed to MutableStateFlow (cause it’s private) even if it was designed to be possible.

What about a shortcut? If it’s simple, why such option is not included by default?

data class Flowable <T>(
    val flow: Flow<T>
){
    val value: T
        get() = runBlocking { return@runBlocking flow.stateIn(CoroutineScope(Dispatchers.Default)).value }

Even @Composable can observe MutableStateFlow in one string. So, you propose to pass observer logic into composables? What about non-Android apps (or apps without UI) then?

Why would you want to do this? It completely violates structured concurrency.

I assume the thing that is observing the flow has some lifetime, a point at which it starts and a point after which it should go away. Create a CoroutineScope when you want to start, launch a coroutine in it to observe the flow, and then cancel the scope when the lifetime ends. If it truly has no end then you can use GlobalScope.

4 Likes

We had a LifeData that can observe anything without extra coroutines, right?
We have Compose that can observe StateFlow without extra coroutines, is it correct?
If so, why shouldn’t we have it simple under the hood in a basic structure?

I understood, real stuff should be posted there.

Frankly, it is hard to answer your questions, because you use coroutines and flows in a way that doesn’t make too much sense. Your above code is pretty much just a convoluted this:

fun getUt() = (bl * 0.2f * up).toInt() 

It does almost the same thing. Even if using flows, your above trick does pretty much the same thing as just bl.first().

So why do you use flows in the first place if you don’t want to use them? Flows are about observing for changes and your main complain here is that you have to observe instead of just getting the value.

Also, YouTrack is not for “real stuff”, but for reporting bugs and improvements. It is not for discussing our concerns or asking why things were designed as they were.

3 Likes

Ahh, ok, I think I now know what you tried to do. “Tried”, because I believe your code doesn’t do what you believe it does. As said above, it just calculates the value when asked.

So your idea is to have reactive/bindable variables, where changing one of them propagates automatically to other ones, right? In that case yes, using MutableStateFlow + stateIn seems like a viable option.

I guess the reason why such a tool is not there already is because in its simplest form it is just: flow1.map { ... }.stateIn(), so this is easy enough and doesn’t require an additional tool. Also, believe it or not, but “problems” you have with flows here are actually their intentional features, not flaws. We used callbacks (as in observeForever() mentioned by you) for tens of years and that was always unreliable, error-prone and hard ro maintain.

5 Likes

Bingo. Was it hard to understand? Actually, my code does what I wanted to with observable variables (at least, visible and testable part of it, but how do you know if your code is best-optimized at all?) even if it uses the current work-around.
Yes, get() = runBlocking { return@runBlocking flow.first() } works as well, with stateIn I just tried to make all variables same MutableStateFlow type and stopped half-way.

Actually, initially I looked at your example in YouTrack. While pretty similar to above code, it works in a much different way. For each call to Flowable.value you create an entirely new StateFlow, wait for the first value and then return it. So this is really like simply: (bl * 0.2f * up).toInt() . Only later I noticed your above code works differently, so I realized you planned to keep the StateFlow with the cached value.

The talk of Flows, Observable and other solutions is a bit misleading. Looking at these topics, you’re likely to get a lot of responses along the lines of “I’m confused–why would you want that?”.

It seems like you don’t want a sequence of values which is evaluated in a suspending context (i.e. you don’t really want flows). And it seems the core behavior you want is not the ability to observe the change on each variable (although that might be part of the solution).

The feeling is similar to someone asking: “I want to paint my house, which airplane should I buy?” and realizing the person thinks they’re supposed to crop-dust the paint down onto their house.

@arocnies how did you delete your message without a sign? Last time I tried, it was unavailable.
Yep, and I see some of the representatives as adepts of delegates and YAGNI. Delegates because they are lazy.

Roman Elizarov commented 28 Oct 2020 20:22

It is in no way a trivial feature. Even delegation to interfaces has a lot of corner cases in its design. It simply makes no sense to even start working on this (hard) design problem, unless there are strong compelling use-cases.
https://youtrack.jetbrains.com/issue/KT-21955/Allow-delegation-to-classes#focus=Comments-27-4475653.0-0

P.S. Nothing personal, but these delegates s**ks. I spent a lot of time to implement them and then reverted back to interfaces because of override problems. It’s even better to make an init() method in an interface.

Hm, I wanted to simplify the whole construction by a single Flow extension

val <T> Flow<T>.value: T get() = runBlocking { return@runBlocking this@value.first() }

Or at least, inherit my class from it, but found it’s an interface and the real thing is private (again!) StateFlowImpl that is available through a wrapper - the MutableStateFlow. It’s a quite big class to copy-paste (or I could just override the collect method, but it’s also not fine to copy-paste much), so my construction became quite rigid and I think - it doesn’t worth it.

@Test
fun mutableStateFlow() {
    val bl = MutableStateFlow(3)
    val bp = MutableStateFlow(bl.map { it * 0.2f })
    val ut = MutableStateFlow(bp.value.map { (it * up).toInt() })

    assertEquals(6, ut.value.value)
    bl.value = 4
    assertEquals(8, ut.value.value)
}

But at least, we got what we wanted at first - all vars are same MutableStateFlow type and observable.

Now please add a println inside any map {}, get ut.value.value several times and you will notice these maps are invoked for each ut access. So again, this is like simply doing: (bl.value * 0.2f * up).toInt(), without any flows.

The reason why you can’t solve this whatever you try is because instead of trying to learn how to use tools properly, you tend to do things “your way” and try to force tools to do things they were not designed to do. You hit a screw with a hammer, you see it doesn’t work very well, so you assume you just need to hit harder.

The closest solution to what you trying to do is something like:

fun main() {
    val up = 10

    val bl = MutableStateFlow(3)
    val bp = bl.map { it * 0.2f }.toStateFlowBlocking()
    val ut = bp.map { (it * up).toInt() }.toStateFlowBlocking()

    println(ut.value)
    bl.value = 4
    Thread.sleep(100)
    println(ut.value)
}

fun <T> Flow<T>.toStateFlowBlocking() = runBlocking { stateIn(GlobalScope) }

Note we have to wait between setting bl and getting from ut, because flows are by design asynchronous.

Of course, above code is totally against coroutines/flow good practices. We should not use GlobalScope, we should not block the thread while using coroutines. So here’s the question: why do you use coroutines/flows if their main features and main reasons to use them are actually the opposite of what you need? Coroutines/flows are mostly about writing asynchronous code that waits by suspending (not blocking) and with structured concurrency. You want your code to work synchronously, you want to block and structured concurrency is only annoying to you.

My suggestion: either take a step back, spend some time on learning coroutines/flows and/or other asynchronous concepts and use them as they were intended to. Or ignore coroutines/flows entirely, implement a good old observer pattern which should give you exactly what you need. Don’t try to force coroutines/flows to be something they are not.

Also, I don’t understand your concept of MutableStateFlow that observes another variable. Doesn’t it contradict itself? If you say Y is always X * 2 then how could you at the same time keep B settable (mutable)? What should happen if you set X to 5 and Y to 8?

3 Likes

What’s funny is that all this added work may end up causing negative performance hit too :laughing:

In my deleted post I started mentioning a few things but there’s so many layers of assumptions that need to be unwound before getting to something useful.

It’s unfortunate too since if there was a need for doing a lean, graph style calculation then I can imagine some cool solutions are possible. Since OP has made up his mind, I suspect the discussion won’t get to those.

2 Likes

As I said before, I use them because it’s the tool came to replace liveData.

It doesn’t within @Composable, right? Then there’s a place to implement it that way.

1 Like

Oops, accidentally hit the like button and can’t undo it.

I disagree that one can assume @Composible addresses the performance in all cases. Even though a coroutine API replaces livedata it doesn’t mean coroutines should be used to get the behavior you want in regards to caching and updating values.

There still might be a performance hit even with compose.

Yep, the case was: if Composable hit performance observing MutableStateFlow, then it’s fine to do the same in a reasonable range.

How could you observe mutable val?

I’m not sure what do you mean. I mean you said multiple times here that you want ut to be a MutableStateFlow and at the same time its value is based on bl/bp. I think this is self-contradictory. MutableStateFlow is a flow that you set manually by yourself. And your other requirement says its value should depend on another variable. Didn’t you mean ut to be just a StateFlow, not MutableStateFlow?

I mean you both with @arocnies propose to use Observable instead of Flow. How could you observe val a: MutableStateFlow with it? Or how could you observe Observable inside Composable?