Custom Layouts, measuring policies, and BoxWithConstraints in Jetpack Compose
Measuring children according to incoming parent constraints, deferring initial composition.
Layouts and measuring
In Jetpack Compose any Composable that emits UI is defined as a Layout
, regardless of what library it belongs to (foundation, material). The Layout
Composable belongs to Compose UI, which all those libraries depend on for defining their layouts. Think of Box
, Column
, Row
… or any other Composables that emit UI.
When you write a custom Layout
, you get access to a list of elements to measure (measurables
), and some Constraints. Those are Constraints imposed by the parent, or a Modifier
(if there are modifiers affecting the constraints that will be reflected in those Constraints):
Inside the block, you can measure the children using the provided constraints, and then call the layout
function to create the layout and place the children inside. You can read more about this in the official docs. (The trailing lambda in the Layout
Composable is the MeasurePolicy
used to measure the layout and its children).
The way Layout
s are measured in Jetpack Compose is very similar to how View
s are measured: It takes place from parent to children (from top to bottom). When a parent needs to measure its children, it imposes some Constraints
so each child can be measured according to those. Note that by “parent” in this context we can also refer to a Modifier
, not only a parent Layout
, since modifiers can also impose additional constraints to a node (e.g: Modifier.padding
).
🤲 A brief example
Let’s say we want to render a couple of Composables on screen, where each one of them takes half of the available height.
First thought that comes to mind might be measuring screen height then setting half of it to each one of the two Composables. But that would be an ad hoc solution. What would happen if our parent layout didn’t use the whole screen height, like the following one?
Here, the parent takes only half of the screen height. So what we need is a responsive solution that adapts to the available space imposed by the parent.
We could write this using a custom Layout
in a very performant way:
Here we were able to place the two boxes within the custom layout, since all the information we need to create our Composable tree is already known during the Composition. When the custom layout composes it will also compose its children, and to compose the children we don’t need any extra information.
But sometimes we need Composition to actually depend on some value that is not yet available. Imagine that we want to use a different Composable depending on whether we are displaying our UI in a phone or a tablet. We would need to write some conditional logic based on the screen size. But the problem is Composition happens before that information is known. We would need to defer the composition for the children somehow. That is where BoxWithContraints
comes to the rescue.
Conditional Composition
This is the actual use case of BoxWithContraints
.
BoxWithContraints
is a very special Layout
that doesn’t match the behaviour described above, since it does not compose its children during the composition phase. Children of BoxWithConstraints
are composed during the measure/layout stage, not like the rest of the layouts in compose. This is called subcomposition across the codebase, and it is a bit of a performance overhead (that is why writing custom layouts is recommended first when possible).
The creator of a subcomposition can control when the initial composition process happens, and
BoxWithConstraints
decides to do it during the layout phase, as opposed to when the root is composed.
Here is an example of what I’m referring to as “conditional composition”:
In this example we want to make a structural change to the Composable tree based on a condition over a value that we cannot know until the layout phase.
Once we got this, it’s a good time to dig a bit into the sources 👨🏼💻
The sources
Let’s peek into the BoxWithConstraints sources to understand how it works.
The measure policy is where the measuring logic resides. This Composable reuses the same measure policy than the Box
Composable from the Compose Foundation library. This policy makes the box adapt to its wrapped children when they are not set to match the parent size. This policy can be configured in a couple ways for alignment and for propagating the min constraints from the parent. When min constraints aren’t propagated, it will then use loose the minimum constraints used to measure its children (set them to 0). That will let them decide what size they need to take.
constraints.copy(minWidth = 0, minHeight = 0)
The default behavior is actually to not propagate them.
You can learn more about measuring and layout in Compose, measure policies, and this specific one in the Jetpack Compose internals book.
But, how is the initial Composition deferred as mentioned earlier? Well, we only need to step back to the Composable definition to spot it, right at the bottom.
This Composable uses SubcomposeLayout
, which is an analogue of Layout
that creates and runs an independent composition (subcomposition) during the layout phase. This allows child Composables to depend on any values calculated in it, like available width for example. To calculate the measurables it simply calls the subcompose
function to perform subcomposition of the provided content lambda, which will include all its children, and finally proceed to measure them using the calculated measure policy and the incoming parent constraints.
👨🏫 Fully fledge course - “Jetpack Compose and internals”
You might want to attend the next edition of the highly exclusive “Jetpack Compose and internals” course I’m giving in October. I have carefully crafted it so attendees can master the library while learning about its internals in order to grow a correct and accurate mental mapping. This course will allow to position yourself well in the Android development industry. Limited slots left.