Custom Layouts
Every layout we've used so far — Column, Row, Box, even
LazyColumn — is itself built on a single lower-level building block:
Layout {}. Reaching for it directly means writing your own measuring and placement
logic by hand, which is genuinely more work than composing built-in layouts with
weight(), Arrangement, and alignment. Be honest with yourself before
starting: most screens never need this. Reach for a custom Layout
only when you've tried Row/Column/Box combinations and none of them can express the arrangement
you need — a genuinely custom flow, a radial arrangement, or a layout whose sizing depends on a
relationship between children that no built-in arrangement models.
Why Row, Column and Box Aren't Enough Here
RecipeBox tags each recipe with one or more category chips — "Breakfast", "Vegan", "Quick",
"Dessert" — displayed above the ingredients list. A fixed Row lays every chip out on
one line and lets it overflow off-screen; a Column stacks them one per line and
wastes horizontal space. What we actually want is a flow layout: chips fill a row
left-to-right until the next one wouldn't fit, then wrap to a new line — exactly like the flexbox
flex-wrap behavior from CSS. Neither Row nor Column can
express "wrap to a new line based on measured widths," because that decision depends on actually
measuring each child first. That's the job Layout {} exists for.
The Layout Composable: measure, then place
Layout takes the children to arrange (as a normal @Composable lambda)
and a measurePolicy lambda that receives the raw, unmeasured children
(Measurables) plus the incoming Constraints from the parent. Your job
inside that lambda is exactly two steps:
| Step | What you do |
|---|---|
measure() | Call measurable.measure(constraints) on each child to turn it into a Placeable — a child that now has a fixed width/height and can be positioned |
layout {} | Report this layout's own total size, then inside the trailing lambda call placeable.placeRelative(x, y) for every child to position it |
A Placeable only exists after measuring — you cannot place a child before you know
its measured size, which is why the two steps are strictly ordered.
Worked Example: a Wrapping Chip Row for RecipeBox Categories
Here's a simplified flow layout. It measures every chip child with loose constraints (so each chip reports its own natural size), then walks through them accumulating an x-position per line, starting a new line whenever the next chip would exceed the available width:
CategoryFlowRow.kt — a minimal wrapping layout
@Composable
fun CategoryFlowRow(
modifier: Modifier = Modifier,
horizontalGap: Dp = 8.dp,
verticalGap: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
val gapPx = horizontalGap.roundToPx()
val lineGapPx = verticalGap.roundToPx()
// Step 1: measure every chip with loose constraints so each reports its own size
val placeables = measurables.map { it.measure(constraints.copy(minWidth = 0)) }
// Work out which line each chip lands on, and each line's max height
val lines = mutableListOf>()
var currentLine = mutableListOf()
var currentLineWidth = 0
for (placeable in placeables) {
val widthIfAdded = currentLineWidth + (if (currentLine.isEmpty()) 0 else gapPx) + placeable.width
if (widthIfAdded > constraints.maxWidth && currentLine.isNotEmpty()) {
lines.add(currentLine)
currentLine = mutableListOf()
currentLineWidth = 0
}
currentLine.add(placeable)
currentLineWidth += (if (currentLine.size > 1) gapPx else 0) + placeable.width
}
if (currentLine.isNotEmpty()) lines.add(currentLine)
val lineHeights = lines.map { line -> line.maxOf { it.height } }
val totalHeight = lineHeights.sum() + lineGapPx * (lines.size - 1).coerceAtLeast(0)
// Step 2: report our own size, then place every child
layout(width = constraints.maxWidth, height = totalHeight) {
var y = 0
lines.forEachIndexed { index, line ->
var x = 0
line.forEach { placeable ->
placeable.placeRelative(x, y)
x += placeable.width + gapPx
}
y += lineHeights[index] + lineGapPx
}
}
}
}
Using it looks exactly like using Row or Column — the caller doesn't
know or care that a custom measure policy is doing the work underneath:
Rendering RecipeBox category chips
@Composable
fun RecipeCategoryChips(categories: List ) {
CategoryFlowRow(modifier = Modifier.padding(12.dp)) {
categories.forEach { category ->
AssistChip(onClick = {}, label = { Text(category) })
}
}
}
Constraints Flow Both Ways
A subtlety worth calling out: the constraints your measure policy receives already
reflect whatever the parent imposed (a max width from the screen, for example), and the
constraints you pass to each child's measure() call are yours to shape. Passing
constraints.copy(minWidth = 0) above tells each chip "you may be as narrow as you
like, up to the parent's max width" — which is what lets each chip report its own natural,
content-driven size rather than being forced to stretch or shrink unnaturally.
Prefer Modifier.layout {} for a Single Child
If you only need to adjust how one child measures or positions itself — not arrange a
whole group of children — reach for the lighter-weight Modifier.layout {} instead of
a full custom Layout {}. It gives you the same measure/layout {}
callback shape but scoped to a single composable, which is usually all a one-off sizing tweak
needs.
Layout {} often, it's worth double-checking whether a combination of nested Row/Column with weight(), Arrangement.spacedBy(), or a ConstraintLayout could express the same result with far less code to maintain.