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.

Honest framing: This is one of the more advanced corners of Compose. If you've never needed it in a real app, that's completely normal — keep it in your back pocket for the rare case Row/Column/Box can't cover.

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:

StepWhat 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.

Keep this rare: If you find yourself reaching for 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.
Next up: Custom layouts control the tree's structure explicitly. CompositionLocal looks at the opposite extreme — passing data down the tree implicitly, without threading it through every parameter.