I have Column > Box > Column hierarchy. All ...
# compose
r
I have Column > Box > Column hierarchy. All have
fillMaxSize
modifier, while box also has
verticalScroll
. As soon as box has scroll, its child column can no longer be at max size. Is this expected? Feels like a bug to me. Screenshots and code in thread
Copy code
@Composable
fun TestComposable() {
    val scrollState = rememberScrollState()
    Column(
        Modifier
            .fillMaxSize()
            .background(Color(0xFFA05151))
    ) {
        Box(
            Modifier
                .fillMaxSize()
                .verticalScroll(scrollState)
                .background(Color(0xFF1178AF))
        ) {
            Column(
                Modifier
                    .fillMaxSize()
                    .background(Color(0xFF48AF11))
            ) {
                Text(text = "hello", color = Color(0xFFFFFFFF))
            }
        }
    }
}
with
verticalScroll
without scroll
a
This is expected. The content of a scrolling region has infinite max size, and it is not possible to fill an infinite max size.
👍🏽 1
👍 2
r
Thanks! What is recommended way to ensure child has minimum height of its container? For scroll views we had
android:fillViewport
Here, i could use
BoxWithConstraints
, but maybe there is another way?
Copy code
@Composable
fun TestComposable() {
    val scrollState = rememberScrollState()
    Column(
        Modifier
            .fillMaxSize()
            .background(Color(0xFFA05151))
    ) {
        BoxWithConstraints {
            val boxMaxHeight = maxHeight
            Box(
                Modifier
                    .fillMaxSize()
                    .verticalScroll(scrollState)
                    .background(Color(0xFF1178AF))
            ) {
                Column(
                    Modifier
                        .defaultMinSize(minHeight = boxMaxHeight)
                        .fillMaxWidth()
                        .background(Color(0xFF48AF11))
                ) {
                    Text(text = "hello", color = Color(0xFFFFFFFF))
                }
            }
        }
    }
}
a
BoxWithConstraints
is very expensive for this sort of use case, all you're looking to do is propagate the viewport constraints to a lower child during measurement, there's no need to return to composition or defer composition for that, which
BoxWithConstraints
will do.
Something like this should do the trick:
Copy code
var constraints by remember { mutableStateOf(Constraints()) }
    Column(
        Modifier
            .fillMaxSize()
            .onMeasureConstraints { constraints = it }
            .verticalScroll(rememberScrollState())
    ) {
        Box(
            Modifier
                .fillMaxWidth()
                .constrainSize { constraints }
                .background(Color.Blue)
        )
        Box(Modifier.fillMaxWidth().height(100.dp).background(Color.Green))
    }

// elsewhere at the top level...

fun Modifier.onMeasureConstraints(
    block: (Constraints) -> Unit
) = layout { measurable, constraints ->
    // record the constraints *before* measuring so that they're available during recursive measurement
    block(constraints)
    val placeable = measurable.measure(constraints)
    layout(placeable.width, placeable.height) {
        placeable.place(0, 0)
    }
}

fun Modifier.constrainSize(
    getConstraints: () -> Constraints
) = layout { measurable, constraints ->
    val placeable = measurable.measure(constraints.constrain(getConstraints()))
    layout(placeable.width, placeable.height) {
        placeable.place(0, 0)
    }
}
🔥 2
then the two modifiers there and the general technique are reusable wherever else you might want to apply it
you could do things like
fillMaxHeight(0.9)
to make sure that there's always a bit of scrollable content still visible as opposed to filling the entire viewport, etc.
it's specifically important that
constrainSize
defines
getConstraints
as a function and not a raw value, since invoking that function performs a state read of the previously written constraints, invalidating the measurement of the `constrainSize`'d element when the input constraints change, but without invalidating the composition that created it
r
Thanks for such detailed answer! 🙂 Can you elaborate on last bit? Im not sure i understand it. I assume that everything that reads from state container will be invalidated when value inside it changes, right? So the difference here is that if i read raw value - then whole scope that uses
constraints
state needs to be invalidated (which in this example starts with
Column
) since there no other way to trigger change, but if there is a function inside
layout
that reads it, then that invalidation scope is just that
layout
block - so only
Box
and its children will get invalidated?
a
Yes. By using a lambda given to the layout modifier to read the state, you invalidate the layout that calls that lambda function rather than the composition
👍 1
r
I see, will try to keep that in mind. Thanks for explanation 🙂