Building Custom Layouts in Compose: Beyond Row and Column
When Built-in Layouts Are Not Enough
Row, Column, and Box cover 90% of layout needs. But sometimes you need pixel-perfect control: overlapping elements with specific offsets, flow layouts that wrap content, or performance-critical layouts that minimize measurement passes.
Custom layouts in Compose are surprisingly approachable. The Layout composable gives you direct access to measurement and placement, while maintaining the declarative paradigm.
The Layout Composable Basics
Every custom layout starts with the Layout composable. It takes content and a MeasurePolicy:
@Composable
fun SimpleCustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// 1. Measure children
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 2. Decide layout size
val width = placeables.maxOf { it.width }
val height = placeables.sumOf { it.height }
// 3. Place children
layout(width, height) {
var yPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = yPosition)
yPosition += placeable.height
}
}
}
}Real Example: Flow Layout
A flow layout wraps children to the next row when they exceed available width. This is perfect for tag clouds or chip groups:
@Composable
fun FlowRow(
modifier: Modifier = Modifier,
horizontalSpacing: Dp = 8.dp,
verticalSpacing: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
val hSpacingPx = horizontalSpacing.roundToPx()
val vSpacingPx = verticalSpacing.roundToPx()
val placeables = measurables.map { it.measure(constraints) }
var currentX = 0
var currentY = 0
var rowHeight = 0
val positions = placeables.map { placeable ->
if (currentX + placeable.width > constraints.maxWidth) {
currentX = 0
currentY += rowHeight + vSpacingPx
rowHeight = 0
}
val position = IntOffset(currentX, currentY)
currentX += placeable.width + hSpacingPx
rowHeight = maxOf(rowHeight, placeable.height)
position
}
layout(constraints.maxWidth, currentY + rowHeight) {
placeables.forEachIndexed { index, placeable ->
placeable.placeRelative(positions[index])
}
}
}
}Intrinsic Measurements
Sometimes parent layouts need to know child sizes before full measurement. Compose provides intrinsic measurements for this:
- minIntrinsicWidth/Height: Minimum size needed to display content properly
- maxIntrinsicWidth/Height: Size needed to display content without constraints
- Use IntrinsicSize.Min or IntrinsicSize.Max modifiers to query these values