Quick survey/riddle. What do you think this progra...
# compose
a
Quick survey/riddle. What do you think this program will print?
Copy code
@Composable
fun PrintFlowValue(number: Int, flow: StateFlow<Int>) {
    val value by flow.collectAsState()
    println("$number $value")
}

val Zero = MutableStateFlow(0)
val One = MutableStateFlow(1)

fun main() = singleWindowApplication {
    var flow by remember { mutableStateOf(Zero) }
    PrintFlowValue(flow.value, flow)

    LaunchedEffect(Unit) {
        delay(1000)
        flow = One
    }
}
e
I would expect
Copy code
0 0
1 1
a
I think it's 0,0 and 1,1. Since it's a riddle I would expect it won't be as easy but in this case I would blame Jetpack Compose for not being intuitive:)
a
Indeed, unfortunately it’s unintuitive
StateFlow.collectAsState
is essentially implemented like this (details omitted):
Copy code
@Composable
fun <T> StateFlow<T>.collectAsState(): State<T> = produceState(initialValue = value, this) {
    collect { value = it }
}

@Composable
fun <T> produceState(
    initialValue: T,
    key1: Any?,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(key1) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}
Simplifying even further, since our state flow values don’t actually change:
Copy code
@Composable
fun <T> StateFlow<T>.collectAsState(): State<T> {
    val result = remember { mutableStateOf(this.value) }
    LaunchedEffect(this) {
        result.value = this.value
    }
    return result
}
p
At first sight 0 0 1 1
a
That side effect makes me think that there might be extra recomposition, so it's 0,0 twice and 1,1 twice, no?
a
No
a
Hmm , can't really think of anything that wouldn't be a serious issue. Some unconstrained recursion leading to infinite recompositions?
p
Just ran it and I got this:
Copy code
0 0
1 0
1 1
What on earth
This line in
produceState
seems the culprit:
Copy code
val result = remember { mutableStateOf(this.value) }
My theory is it gets the StateFlow from the previous composition and return its value, then after the LaunchEffect runs(after the composition) it updates the StateFlow and return the new value.
a
hey - you took all fun out of it by running it 🙂 the issue reminded me of something similar in RXJava when merging multiple streams with combineLatest I guess it's not really an issue but peculiarity of how things work anyway, it's a good one- thanks for sharing
p
Ah sorry 😔 too much temptation 🤷🏻‍♂️ But TBH I am still not sure what the real cause is. Later, I will log everywhere to see.
a
The reason is here:
Copy code
@Composable
fun <T> StateFlow<T>.collectAsState(): State<T> {
    val result = remember { mutableStateOf(this.value) }
    LaunchedEffect(this) {
        result.value = this.value
    }
    return result
}
The returned state is remembered with no key and is returned before its value is updated.
So when the flow changes, the first time collectAsFlow returns, the value of the state is still the value of the previous flow.
Only when the coroutine of the LaunchedEffect runs, it updates the value.
It’s reported here https://issuetracker.google.com/issues/205590513 and is currently closed as “working as intended”.
a
i guess we can't use remember with the key "this" there since it would defeat the purpose of remembering, but maybe have an option to pass a key to collectAsFlow
a
It should either remember with this as the key (which makes sense; new flow - new state), or remember the flow separately, and when it changes, set the value to the new flow’s value before returning (instead of in the effect).
👍 1
p
Ah right what I suspected, remember positional memoization. It bites me all the time. I keep saying that using
remember
or
LaunchEffect
without keys, is danger.