theapache64
11/10/2024, 5:43 AMmyValue
is always -1
in this case? 🤔PHondogo
11/10/2024, 8:45 AMStylianos Gakis
11/10/2024, 11:52 AMvar recompTrigger by remember { mutableIntStateOf(0) }
recompTrigger
var cache = remember<Int?> { null }
Then you will see the "correct" behaviorStylianos Gakis
11/10/2024, 11:53 AMStylianos Gakis
11/10/2024, 11:54 AMmyValue
is not backed by mutable state, so when it changes compose will not be notified about that change in order to re-read the latest value, recompose, and render the right thing
When you increment recompTrigger, only the recomposition scope inside the button lambda gets recomposed, so your val myValue...
is simply not re-evaluated. This is described much better and in super great detail here https://blog.zachklipp.com/scoped-recomposition-in-jetpack-compose-what-happens-when-state-changes/Stylianos Gakis
11/10/2024, 11:56 AMrecompTrigger
incrementing only invalidates the
{
BasicText("Value is $myValue ($recompTrigger)")
}
part, the part above is not recomposed at all, teherefore getSomeValue()
is not re-runStylianos Gakis
11/10/2024, 11:58 AMmyValue
and then you have successfully accidentally managed to observe the change of a non-mutable state and you do in fact render it on the screen 😄PHondogo
11/10/2024, 11:59 AMPHondogo
11/10/2024, 12:01 PMStylianos Gakis
11/10/2024, 12:04 PMmyValue is not evaluated cause it's first value was captured by lambda.What lambda?
It is not related to state. recompTrigger will trigger recomposition in this caseYeah, but only inside the button, that's what I am saying here
PHondogo
11/10/2024, 12:05 PMStylianos Gakis
11/10/2024, 12:05 PM@Composable
fun Sample() {
var recompTrigger by remember { mutableIntStateOf(0) }
var cache = remember<Int?> { null }
val myValue = cache ?: getSomeValue().also { freshValue ->
if (freshValue != -1) {
cache = freshValue
}
}
BasicText("Value is $myValue ($recompTrigger)", Modifier.clickable { recompTrigger++ })
}
var count = -1
fun getSomeValue(): Int {
count++
if (count == 0) return -1
return count
}
Also makes this "work", the value does update. Again because you are putting everything in one recomposition scope so it all invalidates togetherStylianos Gakis
11/10/2024, 12:09 PMStylianos Gakis
11/10/2024, 12:10 PM@Composable
fun Sample() {
var recompTrigger by remember { mutableIntStateOf(0) }
var cache = remember<Int?> { null }
val myValue = cache ?: getSomeValue().also { freshValue ->
if (freshValue != -1) {
cache = freshValue
}
}
ExtraScope {
BasicText("Value is $myValue ($recompTrigger)", Modifier.clickable { recompTrigger++ })
}
}
@Composable
fun ExtraScope(content: @Composable () -> Unit) {
content()
}
Again, because you're introducing a new recompositon scope which is only around ExtraScope
Stylianos Gakis
11/10/2024, 12:11 PM@Composable
fun Sample() {
var recompTrigger by remember { mutableIntStateOf(0) }
var cache = remember<Int?> { null }
val myValue = cache ?: getSomeValue().also { freshValue ->
if (freshValue != -1) {
cache = freshValue
}
}
ExtraScope {
BasicText("Value is $myValue ($recompTrigger)", Modifier.clickable { recompTrigger++ })
}
}
@Composable
inline fun ExtraScope(content: @Composable () -> Unit) {
content()
}
so adding an inline
in that function will once again make the value update properly. Again, because they are once again all in one recomposition scope, and reading recompTrigger
will invalidate the entire contentPHondogo
11/10/2024, 12:12 PMPHondogo
11/10/2024, 12:13 PMStylianos Gakis
11/10/2024, 12:17 PMgetSomeValue()
and see if it triggers or not.
Let's change that function to this:
fun getSomeValue(): Int {
count++
Log.d("tag", "count:$count")
if (count == 0) return -1
return count
}
With that change, let's have this without the inline
keyword
@Composable
fun ExtraScope(content: @Composable () -> Unit) {
content()
}
When I run it this way, every time I click on the button nothing gets printed on the console. So it's the other way around of what you are describing. It's not that the entire composable recomposes but the button content is skipped.
it's that the rest of the function does not recompose at all, and only the button lambda is recomposing reading the new state.Stylianos Gakis
11/10/2024, 12:18 PM@Composable
inline fun ExtraScope(content: @Composable () -> Unit) {
content()
}
every time I click on the button I see the log printed on every frame.Stylianos Gakis
11/10/2024, 12:19 PM@Preview
@Composable
fun Sample() {
var recompTrigger by remember { mutableIntStateOf(0) }
var cache = remember<Int?> { null }
val myValue = cache ?: getSomeValue().also { freshValue ->
if (freshValue != -1) {
cache = freshValue
}
}
ExtraScope {
BasicText("Value is $myValue ($recompTrigger)", Modifier.clickable { recompTrigger++ })
}
}
@Composable
inline fun ExtraScope(content: @Composable () -> Unit) {
content()
}
var count = -1
fun getSomeValue(): Int {
count++
Log.d("tag", "count:$count")
if (count == 0) return -1
return count
}
And run it on a preview in your IDE to see it for yourself.Stylianos Gakis
11/10/2024, 12:28 PMvar someAnimatedValue = transition.animateFloat(0f, 1f, infiniteRepeatable(tween(100))).value
to your function and see the myValue
skyrocket.
This is a good reminder to not do side-effects in composition (so never do what getSomeValue()
does here.
Read https://developer.android.com/develop/ui/compose/mental-model#simple-example, on the section "This function is fast, idempotent, and free of side-effects."PHondogo
11/10/2024, 12:33 PMStylianos Gakis
11/10/2024, 1:01 PMButton
was inline then it would still be considered that Sample
function was reading the state, since it'd all be one recomposition scope.
Compose is tricky like that in these scenarios. But this really is a result of many mistakes being done in the first place. Side effects in composition. Mutating local vars that are not backed by mutable state. If you just always avoid side effects and make sure that all of your state is either just immutable or is backed by mutable state, all of this headache is fully avoided.Pablichjenkov
11/12/2024, 2:28 AMDonut Skipping
right?
I think bellow article is good:
https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose
I think Google documentation should have a dedicated section for these optimizations.
Another one that gets me very often is positional memoization, typically when using remember without keys, and the remember block depends on a Composable function parameter.