Hi guys, I have one question about compose recompo...
# compose
m
Hi guys, I have one question about compose recomposition mechanism and I'm not sure if the following is intended behaviour or bug. It is short snippet. Posting it with details to the 🧵
I have the following code:
Copy code
@Composable
fun Main() {
    val state = remember { mutableStateOf(false) }

    BooleanTest(
        changeValue = { state.value = it },
        booleanValue = state.value,
    )
}

@Composable
fun BooleanTest(
    changeValue: (Boolean) -> Unit,
    booleanValue: Boolean
) {
    Timber.d("Value: $booleanValue")
    changeValue(true)
    Timber.d("Value: $booleanValue")
}
The output of this code is
Copy code
Value: false
Value: false
which is quite confusing because this (but more complex) caused some bugs in our code. We changed state in our viewmodel but it wasn't propagated due this. I would expect the output of this be
Copy code
Value: false
Value: false
Value: true
Value: true
I'm thinking about this somehow incorrectly or is this intended behavior? This thing is fixed when I place
changeValue
call to the SideEffect. Then it works fiine. But still, didn't expect such behavior.
s
Do not do side effects from inside composition https://developer.android.com/develop/ui/compose/side-effects
https://developer.android.com/develop/ui/compose/mental-model#simple-example "This function is fast, idempotent, and free of side-effects."
m
Thanks. I know these things and understand how to fix that. But this is the first time I found issue like that and I would love to understand it and why is it happening
m
Like Stylianos Gakis said, composable functions aren't allowed to mutate data directly. The compose compiler transforms your code in all kinds of fancy ways in order to optimize recomposition, and all of those transformations are based on the assumption that composable functions don't mutate data. If you mutate data, the results are weird and unpredictable (and I suppose could potentially change in different versions of Compose if new optimizations are added to the compiler). So it's not really good to use this behavior to try to understand how compose works, because it's not intended to be used this way. My guess would be that Compose in some cases optimizes away checking for changes to the data that happen during execution of a composable function because the composable function isn't allowed to touch the data (so why bother wasting time checking for it). I'm guessing you already know that putting the change inside a
LaunchedEffect {}
or
Button {}
or
.clickable {}
or something like that will solve the problem. (As an aside, I kind of wish that there were some way for code that mutates state directly in a composable function to be a compile error, to avoid mistakes!)
m
Yeah, I thought that it is gonna be something like that. Some internal compiler "magic" which is caused without any explicit explanation. My guess is that it is caused by mechanism responsible for recomposition. Since at the end of the composition the value is true then it won't run the composition again. But still, wouldn't expect such behavior. I posted it in our internal channel and so far nobody correctly guessed what will be the output of this code.
z
It’s perfectly fine to mutate snapshot state from composition, we do it all the time. Just changing snapshot state isn’t considered a side effect since the snapshot system isolates those changes. Changing any other type of state is. You can’t rely in general that a callback function will do that exclusively though, so if BooleanTest were real it would probably be discouraged or at least need very strong documentation that the callback is not allowed to perform side effects.
The fact that changing a state in the same composition where the state is created doesn’t trigger recomposition where it’s read is maybe a bit surprising. I don’t think this is a technical limitation. I think that behavior might have been chosen to prevent a frame from getting stuck in an infinite loop if a state were to change every time (wouldn’t happen in your case because you’re only setting to true). I’d need to dig into the impl more to confirm.
s
It’s perfectly fine to mutate snapshot state from composition, we do it all the time
Okay you are shattering my world view, care to explain a bit here? Any examples where this is done I can look at?
z
rememberUpdatedState is the canonical one, both old and new text fields (although the old one I think also updates non snapshot state, which is only one of the things it had to go)
s
Aha okay wait, that one however sets the state of a MutableState to a specific value, and if many recompositions happen at the same time it won’t go crazy since every other set will simply do nothing since the MutableState will just be
==
to the old one. That one makes sense and does not look scary at all
I was mostly thinking calling lambda callbacks in composition which you do not even know on the other end what they may be doing, which was the very scary part of the code shared above.
z
Composable functions are always ran in snapshots. Any snapshot state that is changed by a composable will be ignored if that composition is discarded.
Yes, as I said BooleanTest as written would be sketchy since typically functions passed to composables are allowed to perform side effects. You could document it very strongly but it would probably be ignored.
Fun fact I believe the original basic text field made this mistake also
😅 1
s
Fun fact indeed 😅 Okay I think your explanation makes a lot of sense, thanks Zach. The real takeaway here I think should still be to very very strongly oppose calling lambdas in composition because no matter how sure you think you are that assumption may break in the future.
z
Yes correct
c
Okay you are shattering my world view, care to explain a bit here? Any examples where this is done I can look at?
hahaha. glad you asked stylianos because i was also 🤯
Composable functions are always ran in snapshots. Any snapshot state that is changed by a composable will be ignored if that composition is discarded.
I think i knew that but forgot. thats a good one to remember.
m
changing a state in the same composition where the state is created doesn’t trigger recomposition where it’s read
Ahh, this is very useful to know! And explains a lot of things.....
z
I am not 100% on the details there, but based on the behavior you’re seeing I’m guessing it is intentional. @Adam Powell or @Leland Richardson [G] can confirm.
👀 1