https://kotlinlang.org logo
Title
a

Alexander Maryanovsky

03/08/2023, 12:05 PM
Hmm, I’m probably missing something, but it seems wrong that in these two cases
boundsInWindow
is not recomputed when the size changes (🧵)
Case 1 (just “remember”):
@Composable
fun DerivedStateTest(){
    var size by remember { mutableStateOf(180f) }

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ){
        var coords by remember { mutableStateOf<LayoutCoordinates?>(null) }
        val boundsInWindow = remember(coords) {
            println("Recomputing")
            coords?.boundsInWindow()
        }

        println("coords: $coords")
        println("coords.boundsInWindow: ${coords?.boundsInWindow()}")
        println("boundsInWindow: $boundsInWindow")

        Box(
            modifier = Modifier
                .size(size.dp)
                .background(Color.Red)
                .onGloballyPositioned {
                    coords = it
                }
        ){
            Column(
                modifier = Modifier.align(Alignment.Center)
            ){
                Text(
                    text = "Drop here",
                )
                Button(
                    onClick = { size = 380-size }
                ){
                    Text("Change size")
                }
            }
        }
    }
}
Here I can at least understand why it’s happening, although it seems like a design bug. The
LayoutCoordinates
object is actually
InnerNodeCoordinator
which doesn’t actually change. It’s the same object on every
onGloballyPositioned
call.
But then it also doesn’t recompute
boundsInWindow
like this (Case 2)
val boundsInWindow by remember {
            derivedStateOf {
                println("Recomputing")
                coords?.boundsInWindow()
            }
        }
Shouldn’t
derivedStateOf
record the reads of the actual fields of
LayoutCoordinates
and re-run the computation when they change?
Ah, I think I see. What
boundsInWindow
looks at isn’t “state”. It’s
NodeCoordinator._rectCache
which is just a regular
MutableRect
So it seems like a bug
e

efemoney

03/08/2023, 1:20 PM
Why not just store the bounds in window instead of the coords? 🤔
a

Alexander Maryanovsky

03/08/2023, 1:20 PM
Maybe we need both 🙂
e

efemoney

03/08/2023, 1:23 PM
Ah thats true. Anyways, it doesnt seem like a bug to me, since theres nothing in the public API that says that its driven via snapshot state. Possible t file a feature request though
a

Alexander Maryanovsky

03/08/2023, 1:35 PM
A long, long time ago (about a year), @Adam Powell challenged me to find any APIs in compose that expose something other than either immutable state or state that notifies of changes (i.e.
compose.runtime.State
).
Now, obviously nowadays I understand he was right, but here we seem to have an example where it doesn’t hold.
e

efemoney

03/08/2023, 1:51 PM
Understood. I believe i saw somewhere in that thread that there will be exceptions to the “_rule”_ where it makes sense, this is possibly one of those cases.
a

Alexander Maryanovsky

03/08/2023, 1:58 PM
If there’s an exception, it should be heavily documented as such
l

Loney Chou

03/08/2023, 4:27 PM
Note that under the hood
LayoutCoordinates
are `NodeCoordinator`s which won't change. And since
LayoutCoordinates
are not
@Stable
, retrieving info from them is not guaranteed to be seen as state reads, so
derivedStateOf
won't work. You need
Modifier.onPlaced
to reach this goal since it will be called on each placement.
z

Zach Klippenstein (he/him) [MOD]

03/08/2023, 4:55 PM
I don’t love the fact that
LayoutCoordinates
don’t work with the snapshot state notification system either, although it is that way for a good reason: performance. The easiest way to handle this is to put the entire
LayoutCoordinates
object in a
mutableStateOf(…, neverEqualsPolicy())
, the key being that
neverEqualsPolicy
, so that the object will notify changes every time you write to it. Then your
derivedStateOf
should work as expected.
since
LayoutCoordinates
are not
@Stable
, retrieving info from them is not seen as state reads
Just to clarify,
@Stable
has nothing to do with whether something is or isn’t seen as a state read, or the snapshot system at all. A state read is always seen as a state read, and adding
@Stable
to something that’s not backed by snapshot state won’t magically add change notifications. That annotation is only a hint to composable functions about whether they can skip recomposing or not when passed the same object as a parameter.
a

Alexander Maryanovsky

03/08/2023, 5:25 PM
Thanks for the explanation. I wish it was this clear in the docs.
l

Loney Chou

03/09/2023, 12:03 AM
Sorry for being obscure 😅. When I said "since
LayoutCoordinates
are not `@Stable`", I meant "don't expect
derivedStateOf
to rerun if you retrieve info from this class". Because one of the
@Stable
requirements is "Changing to public properties should notify composition".