Understanding Jetpack Compose Layout Process: Composition, Measurement, and Modifier Nodes
Jetpack Compose’s layout process consists of three steps—measuring children, deciding the parent’s size, and placing children—driven by a MeasurePolicy that respects explicit Constraints, while Modifier nodes adjust those constraints and intrinsic measurements provide size hints, enabling efficient, declarative UI rendering.
Compose’s rendering lifecycle consists of three stages:
Composition : After a composable function runs, it generates a LayoutNode tree, which is called the Composition.
Layout : The tree is traversed depth‑first to measure each child’s size and place it within its parent.
Drawing : Using the size and position information from the layout phase, the UI is drawn on screen.
Compared with the classic Android view system (Measure → Layout → Draw), the Composition stage is unique to Compose and enables declarative UI through function calls. The drawing phase is similar to the traditional view system and is omitted here.
The focus of this article is the Layout phase. Compose’s Layout combines measurement and placement, sharing similarities with Android View but also offering distinct advantages.
2. Compose Layout Process – Three Steps
The layout of a node proceeds through three steps:
Measure children : Recursively traverse child nodes and measure their dimensions.
Decide own size : Determine the current node’s size based on the measured sizes of its children.
Place children : Position child nodes at appropriate relative coordinates.
Below is a visual illustration of a card layout and the corresponding node‑tree traversal.
Step‑by‑step analysis of the node tree:
Step1 : Starting from Row , measure its children Image and Column .
Step2&3 : Image has no children, so it only measures its own size and finishes placement.
Step4 : Column measures its children recursively.
Step5&6 : Measure the first Text leaf node.
Step7&8 : Measure the second Text leaf node.
Step9 : Column computes its size (width = max(child1.w, child2.w), height = sum(child1.h, child2.h)) and places the two Text nodes vertically.
All composables eventually call a common Layout composable, which creates a LayoutNode stored in the composition tree.
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}Parameters of Layout :
content : Defines child composables that become the node’s children after composition.
measurePolicy : Encapsulates the three‑step layout logic.
modifier : A chain of modifiers that participates in layout or drawing.
The measurePolicy and modifier are stored on the current LayoutNode and are consulted when measurement begins.
3. MeasurePolicy – Measurement Strategy
fun interface MeasurePolicy {
fun MeasureScope.measure(
measurables: List
,
constraints: Constraints
): MeasureResult
}The measure function receives two important parameters:
measurables : The child nodes awaiting measurement.
constraints : The size limits (min/max width and height) imposed by the parent.
class Constraints {
val minWidth: Int
val maxWidth: Int
val minHeight: Int
val maxHeight: Int
...
}4. Constraints – Measurement Limits
Parents constrain children via Constraints . Unlike the traditional Android View.MeasureSpec , Compose’s explicit constraints enable deep nesting without performance penalties.
For a root node, the Activity window’s dimensions become the maximum constraints. A vertically scrollable container receives an infinite height constraint, allowing it to span multiple screens.
Modifiers also affect constraints. For example, fillMaxWidth forces the node to match the parent’s width by setting minWidth and maxWidth to the parent’s max width.
5. Three‑Step Implementation – Kotlin Advantages
The following Kotlin snippet shows how the three steps are implemented inside a custom measurePolicy :
measurePolicy = { // this: MeasureScope
// Step1: Measure each child
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// Step2: Decide own size
val height = placeables.sumOf { it.height }
val width = placeables.maxOf { it.width }
layout(width, height) { // this: Placeable.PlacementScope
// Step3: Place children vertically
var yPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = yPosition)
yPosition += placeable.height
}
}
}Key points:
Each Measurable provides a measure method that returns a Placeable containing measured width and height.
The parent aggregates child sizes to compute its own dimensions and then calls layout to create a LayoutNode .
Kotlin’s strong type system guarantees that measure is called before layout , preventing ordering mistakes.
6. Modifier Node
Modifiers become nodes attached to the layout tree. The modifier chain creates a single‑direction inheritance tree, with the composable as the leaf.
For example, an Image composable may have a chain like clip → size → … , each represented by a ModifierNode . Modifier nodes participate in the traversal during layout and drawing, allowing them to adjust constraints before the wrapped composable is measured.
// Example of a padding modifier
fun Modifier.padding(
start: Dp = 0.dp,
top: Dp = 0.dp,
end: Dp = 0.dp,
bottom: Dp = 0.dp
) = this then PaddingElement(
start = start,
top = top,
end = end,
bottom = bottom
)
private class PaddingElement(
...
) : ModifierNodeElement
()
private class PaddingNode(
...
) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// custom measurement logic
}
}Modifier nodes are divided into LayoutModifierNode (affecting measurement) and DrawModifierNode (affecting drawing). Layout nodes receive a single Measurable because they form a linear chain, unlike a LayoutNode that may have multiple children.
7. Modifier.layout {}
A quick way to inject custom layout logic is the Modifier.layout {} extension:
fun Modifier.layout(
measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
)Example that adds a 50‑pixel vertical padding:
Box(
Modifier
.background(Color.Gray)
.layout { measurable, constraints ->
// add 50px vertical padding
val padding = 50
val placeable = measurable.measure(constraints.offset(vertical = -padding))
layout(placeable.width, placeable.height + padding) {
placeable.placeRelative(0, padding)
}
}
) { /* content */ }8. Modifier Layout Flow
The diagram below shows a top‑down measurement flow when fillMaxSize is applied with an initial constraint of w:0‑200, h:0‑300.
Each modifier adjusts the constraints before passing them downstream, eventually reaching a leaf node that performs the actual measurement.
9. Intrinsic Measurements – “Intrinsic Size”
Compose enforces the rule “each node is measured only once”. To handle cases where a parent needs size information from its children (e.g., aligning a Divider with surrounding Text ), Compose introduces intrinsic measurements.
Before the regular measurement pass, the parent can query children for intrinsic dimensions via methods such as IntrinsicMeasureScope.minIntrinsicWidth , maxIntrinsicHeight , etc. These values are independent of constraints and allow the parent to compute more appropriate constraints for the final measurement.
fun interface MeasurePolicy {
fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List
,
height: Int
): Int
fun IntrinsicMeasureScope.minIntrinsicHeight(...)
fun IntrinsicMeasureScope.maxIntrinsicWidth(...)
fun IntrinsicMeasureScope.maxIntrinsicHeight(...)
}Using Modifier.height(IntrinsicSize.Min) on a Row forces the row to adopt the maximum intrinsic height of its children, ensuring that a Divider aligns correctly with the surrounding text.
Thus, intrinsic measurements provide constraint‑free size hints that enable accurate layout while still respecting the “measure once” principle.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.