Custom Layouts & Modifiers in Compose
Build custom layouts and modifiers in Jetpack Compose — Layout composable, custom modifiers, intrinsic measurements, and when you need to go beyond Column and Row.
This is Part 8 of a 10-part series on Jetpack Compose.
- Your First Screen
- State Management
- Navigation
- Lists
- Theming
- Forms
- Side Effects
- Custom Layouts (this post)
- API Integration
- Migration from XML
When you need custom layouts
Column, Row, and Box handle 90% of layouts. Custom layouts are for the remaining 10%:
- Overlapping children with custom positioning
- Flow layouts (wrap to next line when full)
- Staggered grids
- Circular arrangements
- Complex measurement logic
The Layout composable
Every layout in Compose follows the same process: measure children → place children.
import androidx.compose.ui.layout.Layout
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun SimpleColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// 1. Measure each child
val placeables = measurables.map { it.measure(constraints) }
// 2. Calculate total size
val width = placeables.maxOfOrNull { it.width } ?: 0
val height = placeables.sumOf { it.height }
// 3. Place children
layout(width, height) {
var y = 0
placeables.forEach { placeable ->
placeable.placeRelative(0, y)
y += placeable.height
}
}
}
}
This is a simplified Column. Each child is measured, then placed vertically one after another.
The measurement contract
- Each child is measured exactly once
- Constraints define min/max width and height
- After measurement, each child reports its size (Placeable)
- In the
layoutblock, you position each Placeable
Flow layout — Wrap to next row
A common custom layout: items wrap to the next row when they don’t fit:
@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 hSpacing = horizontalSpacing.roundToPx()
val vSpacing = verticalSpacing.roundToPx()
val placeables = measurables.map { it.measure(constraints) }
var currentX = 0
var currentY = 0
var rowHeight = 0
var maxWidth = 0
data class Position(val x: Int, val y: Int)
val positions = mutableListOf<Position>()
placeables.forEach { placeable ->
if (currentX + placeable.width > constraints.maxWidth && currentX > 0) {
// Wrap to next row
currentX = 0
currentY += rowHeight + vSpacing
rowHeight = 0
}
positions.add(Position(currentX, currentY))
rowHeight = maxOf(rowHeight, placeable.height)
currentX += placeable.width + hSpacing
maxWidth = maxOf(maxWidth, currentX - hSpacing)
}
val totalHeight = currentY + rowHeight
layout(maxWidth, totalHeight) {
placeables.forEachIndexed { index, placeable ->
placeable.placeRelative(positions[index].x, positions[index].y)
}
}
}
}
Usage:
FlowRow(horizontalSpacing = 8.dp, verticalSpacing = 8.dp) {
tags.forEach { tag ->
AssistChip(
onClick = { /* ... */ },
label = { Text(tag) }
)
}
}
Note: As of Compose Foundation 1.4+, FlowRow and FlowColumn are available in androidx.compose.foundation.layout. Use the built-in version when possible.
Custom modifiers
Simple modifier with Modifier.then
fun Modifier.debugBorder(color: Color = Color.Red) = this.then(
Modifier.border(1.dp, color)
)
// Usage
Text("Debug", modifier = Modifier.debugBorder())
Custom drawing modifier
fun Modifier.dashedBorder(
width: Dp,
color: Color,
dashLength: Dp,
gapLength: Dp,
cornerRadius: Dp = 0.dp
) = this.then(
Modifier.drawBehind {
val stroke = Stroke(
width = width.toPx(),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(dashLength.toPx(), gapLength.toPx())
)
)
drawRoundRect(
color = color,
style = stroke,
cornerRadius = CornerRadius(cornerRadius.toPx())
)
}
)
// Usage
Box(
modifier = Modifier
.size(100.dp)
.dashedBorder(1.dp, Color.Gray, 8.dp, 4.dp, 8.dp)
)
Custom layout modifier
Modifiers can change how a single composable is measured and placed:
fun Modifier.firstBaselineToTop(firstBaselineToTop: Dp) = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
// Find the first baseline
val firstBaseline = placeable[FirstBaseline]
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
placeable.placeRelative(0, placeableY)
}
}
// Usage — align text by baseline rather than top edge
Text(
text = "Hello",
modifier = Modifier.firstBaselineToTop(32.dp)
)
Intrinsic measurements
Sometimes a parent needs to know a child’s size before measuring it. For example: making all items in a row the same height as the tallest item.
@Composable
fun EqualHeightRow(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
content()
}
}
IntrinsicSize.Min tells the Row to query each child’s minimum intrinsic height and use the maximum across all children. All children are then given that height.
Custom intrinsics
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
// Normal measurement
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOfOrNull { it.width } ?: 0
val height = placeables.sumOf { it.height }
return layout(width, height) {
var y = 0
placeables.forEach { placeable ->
placeable.placeRelative(0, y)
y += placeable.height
}
}
}
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
return measurables.sumOf { it.minIntrinsicHeight(width) }
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
return measurables.maxOfOrNull { it.minIntrinsicWidth(height) } ?: 0
}
}
)
}
SubcomposeLayout — Measure before compose
Sometimes you need to measure one child to determine what to compose for another. SubcomposeLayout lets you compose children in multiple passes:
import androidx.compose.ui.layout.SubcomposeLayout
@Composable
fun MatchParentWidth(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
indicator: @Composable (width: Dp) -> Unit
) {
SubcomposeLayout(modifier) { constraints ->
// First pass: measure content
val contentPlaceables = subcompose("content") { content() }
.map { it.measure(constraints) }
val maxWidth = contentPlaceables.maxOfOrNull { it.width } ?: 0
// Second pass: compose indicator with known width
val indicatorPlaceables = subcompose("indicator") {
indicator(maxWidth.toDp())
}.map { it.measure(constraints) }
val height = contentPlaceables.sumOf { it.height } +
indicatorPlaceables.sumOf { it.height }
layout(maxWidth, height) {
var y = 0
contentPlaceables.forEach {
it.placeRelative(0, y)
y += it.height
}
indicatorPlaceables.forEach {
it.placeRelative(0, y)
y += it.height
}
}
}
}
Use SubcomposeLayout sparingly — it’s more expensive than Layout because it can trigger multiple composition passes.
Performance considerations
- Avoid unnecessary custom layouts —
Column,Row,Boxwith modifiers handle most cases - Measure each child once — the layout system enforces this, but complex logic can cause multiple layout passes
- Use
Modifier.layoutfor single-child adjustments — simpler than a fullLayoutcomposable - Cache calculations — if your layout does expensive math, use
remember
Summary
| Tool | Use case |
|---|---|
Layout | Custom multi-child layouts |
Modifier.layout | Custom single-child measurement/placement |
Modifier.drawBehind/drawWithContent | Custom drawing |
SubcomposeLayout | Measure-then-compose patterns |
IntrinsicSize | Query child sizes before measurement |
Most apps never need custom layouts. But when Column, Row, and Box aren’t enough — flow layouts, staggered grids, custom positioning — these tools give you full control.
Next: Compose + Retrofit + Coroutines — Full API Integration.