https://kotlinlang.org logo
#compose
Title
# compose
a

Alexander Maryanovsky

11/30/2023, 7:21 PM
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

eygraber

11/30/2023, 8:11 PM
I would expect
Copy code
0 0
1 1
a

andriyo

11/30/2023, 8:13 PM
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

Alexander Maryanovsky

11/30/2023, 8:33 PM
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

Pablichjenkov

11/30/2023, 9:13 PM
At first sight 0 0 1 1
a

andriyo

11/30/2023, 9:28 PM
That side effect makes me think that there might be extra recomposition, so it's 0,0 twice and 1,1 twice, no?
a

Alexander Maryanovsky

11/30/2023, 9:30 PM
No
a

andriyo

11/30/2023, 9:46 PM
Hmm , can't really think of anything that wouldn't be a serious issue. Some unconstrained recursion leading to infinite recompositions?
p

Pablichjenkov

12/01/2023, 3:21 AM
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

andriyo

12/01/2023, 5:45 AM
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

Pablichjenkov

12/01/2023, 6:04 AM
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

Alexander Maryanovsky

12/01/2023, 6:17 AM
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

andriyo

12/01/2023, 6:22 AM
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

Alexander Maryanovsky

12/01/2023, 6:25 AM
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

Pablichjenkov

12/01/2023, 6:27 AM
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.
6 Views