PCSalt
YouTube GitHub
Back to Jetpack Compose
Jetpack Compose · 2 min read

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.

  1. Your First Screen
  2. State Management
  3. Navigation
  4. Lists
  5. Theming
  6. Forms
  7. Side Effects
  8. Custom Layouts (this post)
  9. API Integration
  10. 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

  1. Each child is measured exactly once
  2. Constraints define min/max width and height
  3. After measurement, each child reports its size (Placeable)
  4. In the layout block, 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

  1. Avoid unnecessary custom layoutsColumn, Row, Box with modifiers handle most cases
  2. Measure each child once — the layout system enforces this, but complex logic can cause multiple layout passes
  3. Use Modifier.layout for single-child adjustments — simpler than a full Layout composable
  4. Cache calculations — if your layout does expensive math, use remember

Summary

ToolUse case
LayoutCustom multi-child layouts
Modifier.layoutCustom single-child measurement/placement
Modifier.drawBehind/drawWithContentCustom drawing
SubcomposeLayoutMeasure-then-compose patterns
IntrinsicSizeQuery 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.