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

Angus L

07/15/2022, 9:46 AM
Hello, I noticed that
LaunchedEffect(someMutableState.value)
will cause a custom
Layout()
in the same
@Composable
function to remeasure every time the value of the key changes. Which was not what I was expecting. This got me wondering if there is something wrong with the way
LaunchedEffect
is used in this case and what the ideal approach to get around this remeasure would be. Wrapping it into a separate
@Composable
or
SubcomposeLayout
would work, but that feels even worse to me.
s

Stylianos Gakis

07/15/2022, 10:20 AM
Maybe what’s happening here is that since you’re reading the .value of counter in the “outer” scope,
Layout
also recomposes since it’s inline and therefore has the same recomposition scope as
MinimalRemeasureExample
. When you do not access the .value of
counter
in that scope, and you only access it inside the
layout(constai…)
part, you’re not invalidating the outer recomposition scope since you’re only reading that state in the placement (I think) step of composition.
a

Angus L

07/15/2022, 10:29 AM
Thanks! From observation it seems unrelated to what triggers the LaunchedEffect, it is just a bit more visible this way since it happens on click. If we split counter and the value that drives the placement it is still happening. Hope I understood you correctly and this is along the lines you meant as well.
s

Stylianos Gakis

07/15/2022, 10:34 AM
Yeah you’re still reading counter.state in the
MinimalRemeasureExample
recomposition scope here which is the same recomposition scope for
Layout
therefore the entire thing is recomposing. Here’s a sample of introducing a new Composable which will not be inline therefore have its own recomposition scope, while also deferring reading the value by using a lambda and only reading it in the
layout()
block, therefore not recomposing the entire
Layout
composable. The output of the above snippet reads:
Copy code
> Task :run
Measuring 0
Placing 0
Counter changed.
Placing 0
Counter changed.
Placing 0
Counter changed.
I super recommend reading Zach’s blogposts that cover all of this, particularly Scoped recomposition in Jetpack Compose — what happens when state changes? for this specific case.
a

Angus L

07/15/2022, 10:37 AM
Thanks for the example, that's what I meant by wrapping it into a @Composable or SubcomposeLayout in the initial message. But as long as the LaunchedEffect is in the same @Composable scope it seems to always trigger measure regardless of if the value is actually used inside that particular layout, which still feels weird to me. So just wrapping the LaunchedEffect into a BoxWithConstraints() will have the same result and eliminate the remeasure.
s

Stylianos Gakis

07/15/2022, 10:41 AM
Right, sorry I didn’t understand that in the first place, my bad! I am personally not aware of a way to introduce a new recomposition scope when you’re calling an inline function that you don’t actually want it to be inline. I hope if there is such an option someone can chime in and show it to us 😄 I wonder what the most “idiomatic” way to do this is. Wrapping the LaunchedEffect into a
BoxWithConstraints
therefore introducing a composable which uses
SubcomposeLayout
feels like a super overkill for something like this. Especially considering all the advice to avoid
SubcomposeLayout
unless absolutely needed. Also just to answer this
regardless of if the value is actually used inside that particular layout, which still feels weird to me.
That’s the thing, since you are calling .value on it for LaunchedEffect you are using it. THe fact that you’re not using it for something that emits some UI doesn’t change the fact that you are reading that state in that scope. So I don’t think this is something that should feel weird to you.
For now I’d just introduce this (or whatever name you like)
Copy code
@Composable
fun NewRecompositionScope(content: @Composable () -> Unit) {
    content()
}
And wrap either your
MyLayout
or the
LaunchedEffect
, maybe like this
Copy code
NewRecompositionScope {
    LaunchedEffect(counter) {
        println("Counter changed.")
    }
}
And it will work. And hopefully if someone else has a smarter idea they can contribute it in this discussion 😄
a

Angus L

07/15/2022, 10:48 AM
100% Agree @ it feeling overkill/even worse with the SubcomposeLayout. Not sure if I understand your last paragraph correctly. Even if you split it inside the composition. Since nothing is changed in the layout/measurement I still feel it is awkward to me 🙂
And yup @ your example of NewRecompositionScope, you would not even need to call content() in this case.
s

Stylianos Gakis

07/15/2022, 11:06 AM
And not sure where you’re going with that last snippet and the two mutableStates. Isn’t this modified version of the original snippet enough? Also yes, you would need to call
content()
since otherwise
LaunchedEffect
could never be called in the first place
a

Angus L

07/15/2022, 11:19 AM
Thanks for the many replies. You're right @ content, sorry I tried this approach as well and thought it worked without it. Just looked it up, I'm calling content as well. @Where I was going: I was trying to get a better sense by separating it more. Still trying to wrap my head around this case I guess. In the end the LaunchedEffect could also just be a Text displaying the counter. In which case just looking at it my gut feeling tells me it should know that only text is affected by that change. But you're most likely right that it is due to layout being inlined and Text then still being in the same scope.
🙏 1
s

Stylianos Gakis

07/15/2022, 11:25 AM
Awesome, I am glad we’ve come to a working solution that we all understand. To avoid the problem with if you want to add another text for example what I’d do is wrap the
Layout
with the
NewRecompositionScope
as that is practically what we want to do, introduce a recomposition scope for the Layout, not for
LaunchedEffect
. Then you’re free to add whatever other composable in that scope. I understand that this feels sub-optimal, I am still hoping that someone can come in with a better solution to this if it exists.
❤️ 1
a

Angus L

07/15/2022, 11:34 AM
Yup! Still hoping to see a different solution as well. But taking layout being inlined into consideration like you said, it might be the only option to wrap it after all. Definitely something to keep in mind, as it is not 100% apparent (at least to me) and can produce weird janks if the measurements are too complex to re-evaluate at lets say a frame to frame base if the value changes at that rate as well.
Copy code
To avoid the problem with if you want to add another text for example what I'd do is wrap the Layout with the NewRecompositionScope as that is practically what we want to do
This seems to behave differently by the way. If I understood correctly what you meant, wrapping the layout does not seem to avoid the remeasure.
Copy code
NewRecompositionScope {
        Layout(
Probably because LaunchedEffect still sits on top in the "hierarchy", so only pushing LaunchedEffect(The Receiver) further down avoids it?
s

Stylianos Gakis

07/15/2022, 12:03 PM
Right! Since the outer scope recomposes, NewRecompositionScope will as well oops 🥴 I forgot that the way I had made this work before is that I extracted another composable but then passed a lambda to defer reading from the MutableState and that composable was I guess skipped since none of its inputs had changed. This isn’t the case in what I suggested last, my bad.
👍 1
a

Angus L

07/15/2022, 12:15 PM
🙂 Just shows that it's not that transparent I guess. Definitely helps to talk about it just to validate whats going on and store it in "deep memory". I'm often not 100% sure I translated/interpreted what I've read in the docs/articles correctly. So thanks a lot for your time.
🙌 1
n

natario1

07/15/2022, 12:24 PM
You can also use LaunchedEffect(Unit) and inside that, snapshotFlow { state.value }.collect { ... }.
👏 1
☝🏻 1
a

Angus L

07/15/2022, 12:39 PM
That seems like a great alternative to wrapping it! Totally did not occur to me.
s

Stylianos Gakis

07/15/2022, 12:42 PM
Nice, I always forget about snapshotFlow. This wouldn't work however if as you said in an example before that was a composable that was emitting UI.
👍 1
z

Zach Klippenstein (he/him) [MOD]

07/15/2022, 8:50 PM
Yea generally snapshotFlow is more efficient for observing state in coroutines because it lets you avoid recompositions.
👍 1
164 Views