What you'll learn
Column, Row, and Box handle 95% of layouts — but when you need something they can't express, or a bespoke graphic like a progress ring, Compose lets you drop down a level. This module demystifies how layout actually works and shows you how to draw anything with the Canvas.
By the end you'll be able to:
- Explain Compose's three rendering phases
- Write a custom layout with the
Layoutcomposable - Measure and place children yourself
- Draw custom graphics with
CanvasanddrawBehind
The three phases
Every frame, Compose runs three phases. Understanding them is the key to this whole module:
- Composition — what to show. Your composables run and build the UI tree.
- Layout — where and how big. Each node is measured, then placed.
- Drawing — how it looks. Each node paints itself onto the canvas.
So far you've worked in composition. Custom layouts hook into the layout phase; Canvas hooks into the drawing phase.
The Layout composable
The Layout composable lets you define your own measuring and positioning logic. It hands you the children as measurables and the space you have as constraints, and you return your size and where each child goes. Here's a from-scratch vertical stack (a simplified Column):
@Composable
fun VerticalStack(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
// 1) Measure each child within the constraints.
val placeables = measurables.map { it.measure(constraints) }
// 2) Decide our own size.
val width = placeables.maxOf { it.width }
val height = placeables.sumOf { it.height }
// 3) Place each child.
layout(width, height) {
var y = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = y)
y += placeable.height
}
}
}
}Measure & place
The custom-layout contract is always the same three steps:
- Measure each child exactly once by calling
measure(constraints), which returns aPlaceable(the child at its chosen size). - Size yourself based on those placeables.
- Place each placeable at an
(x, y)inside thelayout(w, h)block withplaceRelative(which respects right-to-left languages).
Watch out
Drawing with Canvas
For custom graphics, Canvas gives you a DrawScope with primitives — drawCircle, drawRect, drawArc, drawLine, drawPath. A circular progress indicator is a couple of arcs:
Canvas(modifier = Modifier.size(120.dp)) {
// 'this' is a DrawScope: size, center, and draw* functions are available.
drawCircle(color = Color.LightGray, style = Stroke(width = 12f))
drawArc(
color = Color(0xFF6366F1),
startAngle = -90f,
sweepAngle = 360f * progress, // e.g. 0.7 = 70%
useCenter = false,
style = Stroke(width = 12f, cap = StrokeCap.Round),
)
}For lighter needs, the Modifier.drawBehind and drawWithContent modifiers let you draw around an existing composable without a separate Canvas — great for a highlight behind text:
Text(
"Highlighted",
modifier = Modifier
.drawBehind { // draw underneath the text, cheaply
drawRoundRect(Color(0x336366F1), cornerRadius = CornerRadius(8f))
}
.padding(8.dp),
)Tip
Recap & quick check
Key takeaways
- Compose renders in three phases: composition (what), layout (where/size), drawing (how it looks).
- The Layout composable lets you define custom measuring and placement.
- The contract: measure each child once → decide your size → place each child with placeRelative.
- Measure each child exactly once — measuring twice throws and breaks single-pass layout.
- Canvas (DrawScope) draws custom graphics; Modifier.drawBehind draws around an existing composable.
Quick check
1. What are Compose's three phases, in order?
2. In a custom Layout, how many times may you measure each child?
3. What does measure(constraints) return?
4. Which draws custom graphics like a progress ring?
5. When should you build a custom layout?
You can build any UI Compose can render. To finish Phase 5, let's make sure it all runs fast — performance and best practices.