---
title: 'Basic Layouts'
source: 'https://academia.sh/en/courses/compose/basic-layouts'
course: 'UI Development with Jetpack Compose'
language: en
updated: '2026-10-21T05:02:38+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes how column, row and box layouts arrange their children'
    - "Measures that in the layout's single pass the constraint travels down, the size travels up and the position travels down"
    - 'Shows that reordering children changes the position but not the direction'
    - 'Verifies that alignment enters only the position calculation'
---

# Basic Layouts

Measuring which direction constraints, sizes and positions travel in the UI tree for column, row and box layouts.

The previous lesson measured how a change can quietly travel out to its owner. This lesson looks at a value that travels the opposite way in the same tree: the limit a parent gives its child. A column layout passes the width it was given to its children; the children stay within that width and report their own heights; the column then derives each child's position from those heights. The measuring axis is the same throughout the course: which direction does this value travel in the UI tree? There are four directions. A value that goes from a parent straight to its child travels **down**; one that goes from a child to any ancestor in its parent chain travels **up**; one that goes from an ancestor to a descendant, skipping the parent, travels **deep**; and if either end is not in the tree, it travels **outside**.

This lesson's question is the three values of the layout pass: constraint, size and position. All three move between the same two nodes, but not in the same direction. Which one decides the lesson's direction is settled in the last section. First, the core that does the measuring:

```kotlin
// Direction.kt — classifies which direction a value travels in the UI tree
class Tree(private val parentOf: Map<String, String>) {
    fun contains(node: String): Boolean = node in parentOf.keys || node in parentOf.values
    fun parent(node: String): String? = parentOf[node]
    fun ancestors(node: String): List<String> {
        val chain = mutableListOf<String>()
        var current = parentOf[node]
        while (current != null) {
            chain.add(current)
            current = parentOf[current]
        }
        return chain
    }
}

class Move(val label: String, val from: String, val to: String, tree: Tree) {
    val direction: String = when {
        !tree.contains(from) || !tree.contains(to) -> "outside"
        to in tree.ancestors(from) -> "up"
        tree.parent(to) == from -> "down"
        from in tree.ancestors(to) -> "deep"
        else -> error("unrelated: $from and $to")
    }

    fun describe() {
        println("$label: from=$from | to=$to -> $direction")
    }
}

fun countDirections(moves: List<Move>) {
    for (name in listOf("down", "up", "deep", "outside")) {
        println("$name: ${moves.count { it.direction == name }}")
    }
}
```

## Column, Row and Box

All three basic layout functions are composable functions (`@Composable`) and take their children in a lambda. `Column` arranges its children from top to bottom, `Row` from start to end (left to right in a left-to-right language); `Box` stacks its children on top of one another. The axis along which they are arranged is the main axis, and the axis perpendicular to it is the cross axis. The distribution of space along the main axis is given by `Arrangement`, and the position along the cross axis by `Alignment`.

```kotlin
// Platform code: Column, Row and Box
@Composable
fun Summary() {
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Text("Title")
        Row(horizontalArrangement = Arrangement.SpaceBetween) {
            Text("Name")
            Text("Total")
        }
        Box(contentAlignment = Alignment.BottomEnd) {
            Text("Note")
        }
    }
}
```

This snippet sets up three tree relationships. Each of `Column`, `Text`, `Row` and `Box` is a node in the UI tree; `Text("Title")`, `Row` and `Box` are children of `Column`; the two `Text` calls in `Row` are its children. The order is the order written in the source. The alignment and spacing parameters carry a rule about the node's children; they say nothing about the node itself.

## The Layout's Single Pass

The platform processes a layout node in three steps. First the parent gives the child its constraints: the smallest and largest width and height the child may take. Then the child picks a size within that range and reports it; this is called measuring. Finally the parent looks at the reported sizes and decides each child's position; this is called placing. The three steps happen in a single pass: the parent gives a child its constraint, the child is measured once and placed once.

The model captures this platform rule. The model is not the platform itself; the helper file carries only the column rule. Its input is the parent's width, the `(name, width, height)` triples of the children and the cross-axis alignment. Its output is the `x` and `y` position of each child. `y` is the sum of the heights of the children before it; `x` depends on the alignment. The numbers here are the model's calculation, not pixels measured by the platform.

```kotlin
// Layout.kt — the single pass of a column layout (the model's calculation)
class Placed(val node: String, val x: Int, val y: Int)

fun placeColumn(parentWidth: Int, items: List<Triple<String, Int, Int>>, alignment: String): List<Placed> {
    var y = 0
    return items.map { (node, width, height) ->
        val x = when (alignment) {
            "center" -> (parentWidth - width) / 2
            "end" -> parentWidth - width
            else -> 0
        }
        val placed = Placed(node, x, y)
        y += height
        placed
    }
}
```

The first measurement is the three values of a single child. The tree puts `Screen` under `App`, `Column` under `Screen` and two children under `Column`. `Title` is the first child. The constraint is given from `Column` to `Title`. The size returns from `Title` to `Column`. The position again goes from `Column` to `Title`; the target of the move is the name of the node the model placed.

```kotlin
// Main.kt — the constraint, size and position of a single child
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column", "Body" to "Column"))
    val placed = placeColumn(360, listOf(Triple("Title", 200, 48), Triple("Body", 300, 120)), "start")
    val first = placed[0]
    val moves = listOf(
        Move("constraint-title", "Column", "Title", tree),
        Move("size-title", first.node, "Column", tree),
        Move("position-title", "Column", first.node, tree),
    )
    for (move in moves) {
        move.describe()
    }
    println("${first.node}: x=${first.x} y=${first.y}")
    countDirections(moves)
}
```

```text
constraint-title: from=Column | to=Title -> down
size-title: from=Title | to=Column -> up
position-title: from=Column | to=Title -> down
Title: x=0 y=0
down: 2
up: 1
deep: 0
outside: 0
```

Both ends of the `constraint-title` line are written by hand: this line is written to show the rule; it is not a measurement. The `size-title` and `position-title` lines, which count as measurements, take one end from the node the model placed.

The constraint and the position went down; the size went up. `Title`'s position is `x=0`, `y=0`: the topmost child of the column sits in the top-left corner because the alignment is at the start. The two values that went down carry different data. The constraint says what the child may choose; the position says where the chosen size will be put. Both are in the parent's hands, because the child does not know its own position.

**LC1.** The model reduces the constraint to a single number, the parent's width. On the platform, a constraint is four bounds (the smallest and largest width and height), and the case where the height constraint is unbounded was not measured.

## Order Changes the Position, Not the Direction

`Column` arranges its children in the order they are written in the source. If the same two children are given in reverse order, their positions swap. The measurement below places two children in two orders and classifies each position move in the tree.

```kotlin
// Main.kt — position moves in two orders
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column", "Body" to "Column"))
    val items = listOf(Triple("Title", 200, 48), Triple("Body", 300, 120))
    val forward = placeColumn(360, items, "start")
    val backward = placeColumn(360, items.reversed(), "start")
    val moves = mutableListOf<Move>()
    for ((name, order) in listOf("forward" to forward, "backward" to backward)) {
        for (p in order) {
            moves.add(Move("position-$name-${p.node.lowercase()}", "Column", p.node, tree))
            println("$name ${p.node}: y=${p.y}")
        }
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
forward Title: y=0
forward Body: y=48
backward Body: y=0
backward Title: y=120
position-forward-title: from=Column | to=Title -> down
position-forward-body: from=Column | to=Body -> down
position-backward-body: from=Column | to=Body -> down
position-backward-title: from=Column | to=Title -> down
down: 4
up: 0
deep: 0
outside: 0
```

`Title` is at `y=0` in the forward order and `y=120` in the backward order; `Body` swaps between `y=48` and `y=0`. All four position moves went in the same direction: down. The order changes the value at the end of the move; it does not change which two nodes the move is between. So reordering changes a layout result, not the flow of data.

**LC2.** The model leaves no space between children; main-axis spacing such as `Arrangement` values and `spacedBy` did not enter the `y` calculation.

## Alignment Is Only Position

In `Column`, `horizontalAlignment` picks the position along the cross axis; in `Row`, `verticalAlignment` does the same job on the vertical axis. Because the model builds the column's cross axis as `x`, the same measurement holds for the cross axis of `Row`: the axes swap, the relationship stays the same. The alignment calculation uses the difference between the child's width and the parent's width. That difference is computed in place in the parent; what is sent to the child is the result of the calculation.

```kotlin
// Main.kt — the position under three alignments
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column"))
    val moves = mutableListOf<Move>()
    for (alignment in listOf("start", "center", "end")) {
        val title = placeColumn(360, listOf(Triple("Title", 200, 48)), alignment)[0]
        println("$alignment: x=${title.x}")
        moves.add(Move("position-$alignment", "Column", title.node, tree))
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
start: x=0
center: x=80
end: x=160
position-start: from=Column | to=Title -> down
position-center: from=Column | to=Title -> down
position-end: from=Column | to=Title -> down
down: 3
up: 0
deep: 0
outside: 0
```

The same child got `x=0`, `x=80` and `x=160` under the three alignments. All three moves are the same: from the parent to the child, down. Alignment is not data but a calculation parameter; it sets up no new relationship in the tree. `Box` carries this relationship to the second axis: its children stack in the same box, and `contentAlignment` picks the horizontal and vertical position together.

**LC3.** The model reduces alignment to three values. The full set of `Alignment` values, the start and end meaning in a left-to-right language, and the case where the child is larger than the parent were not measured.

## Row and Box Set Up the Same Relationship

`Row` is `Column` with its axes swapped. The main axis becomes horizontal: the children's widths are summed, and each child's `x` position is the sum of the widths before it. The cross axis is vertical, and `y` depends on the alignment. The shape of the calculation is the same; what changes is only which size is summed and which is compared with the parent. In the tree, nothing changes: `Row` also gives its children constraints, takes sizes from them and places them. The ends of the moves are the same, so their directions are the same.

`Box` behaves differently, but not in terms of tree relationships. None of the children affects the position of another; each child is measured independently within the box's constraint and placed according to `contentAlignment`. The box's own size fits the largest of its children. This is a case where the size goes up and the parent's size comes from its children. But one child's size does not change its sibling's constraint. There is no direct data move between siblings; this boundary will come up often in the coming lessons.

What these three layouts share is that a child does not know its own position. The position arises not as a parameter of the child but as the result of the parent's placing step. If a child wants to say "lean to the right of the screen", it does so with the parent's alignment parameter or with a modifier that widens its own constraint. The subject of modifiers belongs to the second lesson.

**LC4.** No separate model was built for `Row` and `Box`; the calculation described in this section is an account derived from the column model and was not measured.

## The Bounding Measurement

The lesson's direction is chosen by reading the three values together, not a single move. The measurement below counts four moves together: the constraint `Screen` gives `Column`, the constraint `Column` gives `Body`, the size `Body` reports and the position `Column` gives `Body`.

```kotlin
// Main.kt — the direction tally of four moves
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column", "Body" to "Column"))
    val placed = placeColumn(360, listOf(Triple("Title", 200, 48), Triple("Body", 300, 120)), "start")
    val body = placed[1]
    val moves = listOf(
        Move("constraint-column", "Screen", "Column", tree),
        Move("constraint-body", "Column", "Body", tree),
        Move("size-body", body.node, "Column", tree),
        Move("position-body", "Column", body.node, tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
constraint-column: from=Screen | to=Column -> down
constraint-body: from=Column | to=Body -> down
size-body: from=Body | to=Column -> up
position-body: from=Column | to=Body -> down
down: 3
up: 1
deep: 0
outside: 0
```

Both ends of the `constraint-column` and `constraint-body` lines are written by hand: these lines are written to show the rule; they are not measurements. The moves that count as measurements are `size-body` (up) and `position-body` (down), which take one end from the `Body` node the model placed; two of the three downs in the count belong to the demonstration lines.

Three down, one up. The only value that goes up is the size; the constraint and the position go down. A size never exists without a constraint, because a child can pick a size only within the range it was given; so every size that goes up is preceded by a constraint that goes down. That is why the lesson's direction is the constraint's direction: **down**.

**LC5.** The model sets up four moves for one `Column` and a single `Body` under it. As the number of children grows, the number of size and position moves grows with it; this measurement is limited to one child.

## Summary

- This lesson's direction: down. Constraint and position go from the parent to the child; the measured position move goes down as well.
- The layout's single pass carries three values: constraint, size and position.
- The constraint goes from the parent to the child and the position goes from the parent to the child; both are down.
- The size a child reports returns to the parent; that move is up.
- Order and alignment change the position's value, not the move's direction.

## Next Step

The column is not the only node that passes the constraint to a child. Every modifier chained onto a composable function also passes its own constraint on to whatever follows it. When the order changes, the same constraint travels through a different node. The next lesson measures this chain, whose order changes the direction.
