When using `flow.collectAsState` inside a composab...
# compose
z
When using
flow.collectAsState
inside a composable, if the flow I pass in changes, the last value from the old flow seems to be collected again before the new flow kicks off. 🧵👀
If I have a StateFlow which emits 0, collectAsState will collect 0. If I then pass in another StateFlow which emits 1, Ill get value 0 (again) followed by the new: 1. Why is that? Values are correct if I use LaunchedEffect like below. Im guessing because the old effect is cancelled as soon as the key (flow) changes? Using LaunchedEffect like this everywhere is a huge burden though, compared to the ease of use collectAsState allows.
Copy code
LaunchedEffect(flow){
	flow.collect{ 
		// Collects 0 from first, 1 from second
	}
}
I dont know if it matters, but the stateFlow is a timer like this:
Copy code
flow {
    val context = currentCoroutineContext()

    while (context.isActive) {
        val duration = measureTimeMillis {
            emit(definition.tick())
        }

        val delay = definition.interval() - duration
        delay(delay)
    }
}.stateIn(
    scope = scope,
    started = WhileSubscribed(),
    initialValue = definition.tick()
)
There are several timers like this. In this specific scenario a new timer is created, hence a new flow is passed into my composable. Its not the end of the world, but I would like to understand why it happens if anyone can enlighten me. This also works like LaunchedEffect.
Copy code
val tick by key(timer) {
    timer.collectAsState()
}
c
Sorry if it’s an obvious question, but why don’t you use
collectAsState()
? Can you give us more context?
z
I do use it, @Csaba KozĂĄk! My question is why I get an additional emission of 0 with it. This is exactly what Im using currently:
Copy code
val tick by timer.collectAsState()
c
Are you sure not seeing the initial value being collected first?
z
Im seeing 0 from the first flow initially, and whenever I create another timer I get 0, 1, but the new timer only ever emits 1.
c
Can you share more code what is
timer
?
z
Sure, but I wrote most of it above in the
flow {}
block already.
Copy code
data class Timer internal constructor(
    private val active: Boolean,
    private val flow: StateFlow<TimerTick>
) : StateFlow<TimerTick> by flow
Its basically a flow that ensures it only emits 1 tick per second.
Copy code
data class TimerTick internal constructor(
    val duration: Long,
    val remainder: Float,
    val overdue: Boolean
)
c
Sorry i mean what exaclty is
timer
what you collect? Is it a property in the VM?
z
You could say that, although Im using something different than VM:s. Different timers are passed to the composable throughout the apps lifecycle. The composable renders a SomeScreen with the timer property, basically.
c
If you just use this
val tick by timer.collectAsState()
. Your UI should collect exactly what the flow emits. Try to put
onEach { }
before
collectAsState()
and debug if your flow works correctly.
z
I agree! Ive debugged it like that before, but I just did it again to ensure our sanity; the first flow emits 0, the second one 1 (yet I receive 0, 0, 1 with
collectAsState
). Although Im not sure if this is a bug, or just semantics for how flows are collected in compose, two different State<TimerTick> are created with the collectAsState way of doing it, which in turn could be causing the last value to be shown again before 1?
c
Hmm, what the you mean by
first flow
? Don’t you have just one
flow { }
, saved in a property and collected?
z
Thats the thing, each timer comes with its own flow of ticks that gets collected inside compose. E.g. from the top I have something like
Flow<SomeScreen>
which I collectAsState for rendering, and inside
SomeScreen
I have a
Timer
property, whenever I create a new timer, a SomeScreen is emitted with it, and in turn my composable function with the above logic is invoked.
c
When you create the new flow, first the initial value will be collected, which is i guess zero?
z
The new flow only emits 1 🥲
c
Sorry, i am clueless then. 🙃
z
Me too, hopefully someone out there knows 🥲
j
Been facing a similar issue
👀 1
z
I believe it’s because
produceState
does not treat the initial value as a key (see here), with the workaround being to use the
key(){}
function. There was a discussion about this a while back, and a related bug was filed. That bug is specifically for
produceState
though, and I don’t see an argument against making
collectAsState
behave differently so maybe it’s worth filing another bug.
z
Interesting, thanks for clarifying that @Zach Klippenstein (he/him) [MOD]! I also found the
key
workaround, since it doesnt feel like the natural way of using it I filed a bug. I dont have my hopes up considering working-as-intended of produceState, but at least a discussion can be held around it - Im probably not the first or last to run into this.
I added some more context to my post, feel free to have a look at it @Zach Klippenstein (he/him) [MOD]. I found that
collectAsState
will also re-emit the initial value when returning to the composition, regardless of using the
key
block, Id expect to see the last emitted value if anything. Perhaps this also sheds some additional light on
produceState
? In either case, Id be surprised if this was not a bug! mind blown
a
I’m definitely +1 here for
collectAsState
to not return a
State
with an old value. Was very confused when I discovered it. You can literally have
Copy code
MutableStateFlow(n).collectAsState().value
being something other than
n
.
z
I honestly don’t understand why it was intentionally built this way either – it seems surprising to me as well. That bug is already assigned to Adam, he’s out today so let’s discuss next week. I’m not optimistic about this being changed though, since it seems intentional and behavior changes to “fix” things that aren’t considered bugs are discouraged.
a
I can’t imagine the rationale for
collectAsState
doing that. Thinking about it more, it could even be a security problem. What if the function where
collectAsState
is called runs a
SideEffect
that sends the data somewhere (like in https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish)? Now, if the function is, for example,
Copy code
fun handleSensitiveData(targetUrl: Url, sensitiveData: StateFlow<String>)
you can end up sending sensitive data to the wrong URL.
@Zach Klippenstein (he/him) [MOD] Ok, so my bug report where collecting stateflows literally throws a classcastexception was closed as a duplicate of this issue. If it's indeed a duplicate then I really don't see how this behavior can possibly be reasonable. https://issuetracker.google.com/issues/232007227
Ok, so ch...@google.com changed his mind about it being a duplicate 🤷‍♂️