I had a piece of code that behaved in a way that I...
# compose
m
I had a piece of code that behaved in a way that I cannot understand:
Copy code
//causes infinite recomposition
    val playerUiStateWrong by player.observeState()
        .map { PlayerUIState(it, sound) }
        .collectAsState(PlayerUIState(null, null))

    //works fine
    val playerState by player.observeState().collectAsState(PlayerState.Stopped)
    val playerUiState = PlayerUIState(playerState, sound)
I thought they would both do the same thing, but the first one, whenever I use
playerUiStateWrong
as a parameter in a modifier, causes infinite recompositions, whereas the second one works fine.
Assembling an operator chain in a composable function will yield a new Flow instance each time it's recomposed and collectAsState can't tell that it's semantically equal
If you
remember
the assembled flow based on its inputs you'll see the same results as with your second snippet https://kotlinlang.slack.com/archives/CJLTWPH7S/p1622215590179700?thread_ts=1622212535.172400&cid=CJLTWPH7S
m
Okay, this is very easily avoidable, but I understand that it's not by design and is going to be fixed at some point, correct?
a
No, it's by design. If collectAsState did not cancel an old subscription and start a fresh one when the flow being collected changes then its behavior would be incorrect when a new Flow is used, and Flows do not implement equality for collectAsState to tell the difference between a new and very different flow vs. a new but semantically equivalent one.
(Nor would it be particularly practical for Flows to try to implement equality)
m
OK, this makes sense, although it's an easy trap for a compose beginner 🙂 Thank you for explanation@Adam Powell
I wonder if it's something that could be caught by linter
a
Possibly, but it's easy to defeat a lint check or create false positives for something like this. Consider the example from the linked thread a day or so ago; nothing says the ViewModel can't or wouldn't cache returned flows based on id, use shareIn internally, etc. It would catch only very basic cases
😞 1
z
If kotlin had contract metadata or even just an annotation to specify that a function was guaranteed to be pure/side-effect-free (as most of the flow operators are), compose could probably memoise calls to those functions. Has anything like that been considered as a kotlin feature request? (Obviously not for 1.0)
a
Some related things already happen for functions annotated as
@Stable
but it's not at all clear in all cases that any given pure function isn't cheaper to just perform on recomposition as opposed to performing parameter comparisons and storing a bunch of intermediate values in the composition
The motive for memoizing assembled flows is several layers of semantic meaning away from anything compose understands or arguably should understand
I'm not sure it would be an overall win for understanding the mental model of composition if things like flows/collectAsState did some sort of magic thing here that would be somehow different from the basic constructs anyone else would write themselves
👍🏻 1
👍 1
m
The old views did a lot of those magic things which worked for like 70% of cases but made the other 30% much harder to implement. It's better to have an api which is less magical but more predictable and straightforward
👍 1
Take fitsSystemWindows for example