• a

    Angus L

    1 month ago
    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.
  • Stylianos Gakis

    Stylianos Gakis

    1 month ago
    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

    1 month ago
    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.
  • Stylianos Gakis

    Stylianos Gakis

    1 month ago
    message has been deleted
  • 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:
    > 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

    1 month ago
    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.
  • Stylianos Gakis

    Stylianos Gakis

    1 month ago
    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)
    @Composable
    fun NewRecompositionScope(content: @Composable () -> Unit) {
        content()
    }
    And wrap either your
    MyLayout
    or the
    LaunchedEffect
    , maybe like this
    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

    1 month ago
    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.
  • Stylianos Gakis

    Stylianos Gakis

    1 month ago
    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

    1 month ago
    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.
  • Stylianos Gakis

    Stylianos Gakis

    1 month ago
    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.
  • a

    Angus L

    1 month ago
    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.
  • 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.
    NewRecompositionScope {
            Layout(
    Probably because LaunchedEffect still sits on top in the "hierarchy", so only pushing LaunchedEffect(The Receiver) further down avoids it?
  • Stylianos Gakis

    Stylianos Gakis

    1 month ago
    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.
  • a

    Angus L

    1 month ago
    🙂 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.
  • natario1

    natario1

    1 month ago
    You can also use LaunchedEffect(Unit) and inside that, snapshotFlow { state.value }.collect { ... }.
  • a

    Angus L

    1 month ago
    That seems like a great alternative to wrapping it! Totally did not occur to me.
  • Stylianos Gakis

    Stylianos Gakis

    1 month ago
    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.
  • Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    4 weeks ago
    Yea generally snapshotFlow is more efficient for observing state in coroutines because it lets you avoid recompositions.