Hello, I'm currently switching the main screen of ...
# compose
n
Hello, I'm currently switching the main screen of my current project to Compose, and I'm looking for a way to do get the height from a composable to use it in another :
Copy code
Box {
    Column(Modifier.padding(bottom = /* height of the snackbar*/)) {
        //content
    }
    Snackbar {
        Text("My text")
    }
}
what is the best method to do that? a custom layout ? Do you have any resources that could show me some code sample about this?
f
Is there a reason why not to use Column instead of Box to "offset" the content from snackbar?
n
you're right, it's probably a lot easier! But I'm still wondering if there is any way to use the size from a composable elsewhere in the code
f
There is
onSizeChanged
Modifier which will give you the size right after measure.
Invoke onSizeChanged when the size of the modifier immediately after it has changed. If there is no modifier following onSizeChanged, the content size of the layout is reported.
onSizeChanged will only be invoked during the first time measurement or when the size has changed.
n
I saw this, but I wonder how to change the padding of something outside the onSizeChanged block. Maybe with a mutableState ?
f
Yeah, you save the size to mutable state and than use it in padding. Also I think you'll have to multiply it by screen density because the size is probably in pixels
n
yes exactly, right now I found
Copy code
LocalDensity.current.run { coordinates.size.height.toDp() }
but unfortunatelly I get a runtime crash CompositionLocal LocalDensity not present so I'm looking in how to do that
probably because i'm running localdensity.current from the onSizeChange/onGloballyPositionned method which is not a composable πŸ˜„
f
LocalDensity
should be provided by
setContent
method in your
Activity
Oh...yeah πŸ˜„ That's probably it
n
wow it works, I didn't quite get how
state
worked before but this is super powerful ! Thanks for your help
f
No worries, I am glad I could help. If you are unsure about how state works, I would recommend researching a little bit before you get deep into Compose. Right usage of state will shape the way you write your composables πŸ˜‰
The
remember {}
function is really important to understand too if you are starting with Compose
n
yeah I'm getting there slowly
f
Good luck πŸ™Œ
a
Be aware that you shouldn't introduce "backward" data dependencies during layout. If one part of your layout depends on another, you generally want to express that as part of layout itself, not by using
onSizeChanged
to write a state read by a peer/sibling's layout.
Failing to follow this will often introduce tearing or other visual artifacts in your sizing or positioning, as compose won't return to an already sized and positioned element in the same frame.
This all follows from the unidirectional data flow/idempotency principles underpinning compose
n
yes I read the full guide again and I understand that now (kinda)
is it better to do this ?
Copy code
@Composable
fun Content(displaySnackbar: Boolean, paddingBottom: Int, onSnackbarMeasured: (Int) -> Unit) {
    Box {
        Column(
            modifier = Modifier.fillMaxHeight().padding(
                bottom = if (displaySnackbar) with(LocalDensity.current) { paddingBottom.toDp() }
                else 0.dp)
        ) {
            //content
        }
        if (displaySnackbar) {
            Snackbar(modifier = Modifier.align(Alignment.BOTTOM).onSizeChanged {
                onSnackbarMeasured(it.height)
            }) {
                Text("My text")
            }
        }
    }
}

@Composable
fun Screen() {
    val displaySnackbar by remember { mutableStateOf(false) /* Should be real data coming from a viewmodel/livedata/state*/ }
    var paddingBottom by remember { mutableStateOf(0) }
    Content(displaySnackbar = displaySnackbar, paddingBottom = paddingBottom, onSnackbarMeasured = {
        paddingBottom = it
    })
}
or shall the use of "onSizeChanged" prohibited ? my issue here is that the height of the padding is dynamic as the content of the snackbar can be 1 line, 2 line of text, and I want the bottom padding to match this calculated height
a
Why not use an outer
Column
instead of an outer
Box
? You could skip the out of band layout adjustment entirely
The principles of state on display here are useful to know, including the part about inverted data dependencies πŸ™‚
n
Oh yeah so about that, it's simply because I want the snackbar to be displayed above the content
a
Above as in north of, or above as in overlapping in Z space?
n
imagine my "//content" is a scroll, or a list, I want the snackbar to be displayed above the content with a padding
overlapping Z space
a
Because as written the snippet above implements north of, which would be equivalent to an outer column
n
Yeah I forgot about the modifier that put in at the bottom πŸ˜„
f
And what is the transformation you are trying to achieve with the snackbar height? If thit is your use case than simply draw it in the Box with Bottom alignment. Am I missing something?
n
yeah i forgot about that, so basically my content is a scrollable screen, not a list (longer than the height of the screen but not by much). I wanted the user to be able to see the bottom of the "content" by scrolling, instead of having the snackbar hiding part of it and having to dismiss it
I must precise it isn't a "it must behave like that", more like "OK Compose is so cool, let's see if/how I can do that"
a
right. For that, your approach is almost there, but
padding(NNN.dp)
isn't quite smart enough to do it, and you need be deliberate about the order of measurement.
Compose has several separate phases of execution: composition => layout => drawing
n
Yeah, I tought about making a custom layout but it required more works πŸ˜„ so basically it would be the best way to do that ?
a
by creating a new
padding
modifier when the target size changes, you're performing that work during composition, and composition has finished for that frame already if something is receiving a new size. That happens during layout.
what you're looking for is something more like how https://google.github.io/accompanist/insets/ works
which provides a modifier for inset padding that doesn't perform the state read until layout time; the actual modifier itself doesn't get recreated for these changes.
Dodging a snackbar like this is basically the same use case as window insets.
n
I didn't think about that, I'll try to make it work with that
Well after some thinking, I'm still a bit unsure about how to do that, as the InsetsPaddingModifier apply a padding modifier to a composable, the padding value being provided by a CompositionLocal (the LocalWindowInsets.current). Would the solution be to create a custom CompositionLocal that update when the snackbar is displayed ? (because my issue here is that assuming a make an equivalent of InsetsPaddingModifier, applied to the content, I still don't know the height of the snackbar at this point of time) or simply a custom layout where I can measure and place component ?
a
nah for your case you can just pass the modifier down. I think the
Scaffold
already does this for snackbars, it passes a value to the content function that represents the space consumed by things like snackbars
n
oh ok i'll look into the scaffold source
oohhh SubcomposeLayout πŸ˜„
Analogue of Layout which allows to subcompose the actual content during the measuring stage for example to use the values calculated during the measurement as params for the composition of the children.
Possible use cases:
You want to use the size of one child during the composition of the second child.
It does look like what I wanted ! πŸ˜„ I'll look at it, thanks for your help
a
SubcomposeLayout is almost certainly not what you need here πŸ™‚
SubcomposeLayout is for when you want to change structure, not layout behavior of existing content.
Scaffold may well be using it inappropriately here.
n
oh ok !
After writing a long post with my findings and me being lost, I finally understand what you meant by :
which provides a modifier for inset padding that doesn't perform the state read until layout time; the actual modifier itself doesn't get recreated for these changes.
I didn't think
Copy code
modifier = Modifier.fillMaxHeight().padding(
                bottom = if (displaySnackbar) with(LocalDensity.current) { paddingBottom.toDp() }
                else 0.dp)
would recreate the Modifier.padding() each time. So I suppose I can make a custom modifier :
Copy code
private fun Modifier.avoidSnackbar() = composed {
        AvoidSnackbarModifier()
    }
    private class AvoidSnackbarModifier() : LayoutModifier {
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val snackbarHeight = // Find a way to get/obtain snackbar's height here
            val placeable = measurable.measure(constraints.offset(0, -snackbarHeight))
            
            val width = placeable.width
            val height = (placeable.height + snackbarHeight)
                .coerceIn(constraints.minHeight, constraints.maxHeight)
            return layout(width, height) {
                placeable.place(left, top)
            }
        }
    }

fun Screen() {
    Box {
        Column(modifier = Modifier.avoidSnackbar()) {    }
        Snackbar() {}
    }
}
But now, I'm still uncertain about how to "give" the Snackbar height to my custom avoidSnackbar() modifier. Should I use the same instance and give it to both ? (I don't think it's good idea?!)
Copy code
fun Screen() {
    val modifier = Modifier.avoidSnackbar()
    Box {
        Column(modifier = modifier) {    }
        Snackbar(modifier = modifier) {}
    }
}
I also saw the ParentDataModifier which allows to put data for the parent layout to place the child, but it would require a custom layout or a Modifier.layout { } on my Box layout to make it work? I'm trying hard to understand how everything works here but I'm still unsure of how to achiveve the right way to do what I wanted πŸ˜„ If you have any lead (doc, example), it would be great (if you have time to answer) !
a
you can just use
Modifier.layout {}
to define a layout modifier inline rather than declare a whole class for it, and there's nothing there that looks like it needs
composed {}
there are a lot of ways to approach this, many of which depend on the abstractions and data flow you want to express. The accompanist insets model is likely to be adopted into the compose-foundation and compose-material libraries themselves down the road
n
but it's actually kinda similar to what I did so I'm unsure about what should be done better honestly! Sorry I'm still new to Compose and I think I need to read a lot more about the different phases; anyway, it kinda works for now (and what is great is that it also works with the scaffold component) and I'll take a look another time thanks for your help!
πŸ‘ 1
a
the distance between something that works well enough but is suboptimal and something that you should actively avoid in favor of a better solution is different than it was for a lot of things with views πŸ™‚ I'd mostly class this in the former category for the use case
Copy code
with(LocalDensity.current) { something.toDp() }
☝️ this is a sign that something is trying to move backwards through the phases and suggests that there's probably a way to stay in the later phases for whatever is going on
n
ok ! I'm happy because I learned a lot in the last two days, and I will definitely be ready/ok when compose will be in RC/released
πŸ˜„ 2