Pocket Studio Academy
HomePart 33.4

Column, Row and Box

Full course11 min read·3 questions

Three containers arrange everything in Compose: down, across, and on top of each other. Learn how they space and align their children, what weight really does, and why two composables side by side overlap until you put them in one.

Two composables walk into a setContent

Try this in your head. What appears?

kotlin
1setContent {
2    Text(text = "PLAYER 1")
3    Text(text = "PLAYER 2")
4}

Most people say "two lines of text". What you actually get is one illegible mess in the corner — both pieces of text drawn on top of each other at the top-left, overlapping.

That is not a bug. It is Compose refusing to guess. You said "there is text here" twice and never said where. Arranging things is a separate job, done by a separate composable, and there are exactly three you need:

ContainerPuts its children
Columnone below the other
Rowone beside the other
Boxone on top of the other

That is the entire system. Everything else — grids, cards, lists, that fiddly screen you saw in an app once — is these three, nested.

Think of it like this

Think about a table with books on it.

Column is a pile: put a book down, put the next one on top, and you read the spines going down the stack. Add one and everything below stays put.

Row is a shelf: books side by side, left to right.

Box is what happens if you just drop them all in the same spot: they land on top of each other, and the last one you dropped is the one you see. That sounds useless until you want a badge on a corner of a photo, or a "YOU WIN" card floating over a dimmed game — then it is exactly right.

Nesting is the trick. A shelf of piles. A pile of shelves. Real screens are three or four levels of this and nothing more clever.

Column: stacking downwards

kotlin
1Column(
2    modifier = Modifier.padding(20.dp)
3) {
4    Text(text = "DICE DUEL")
5    Text(text = "First to 30 wins")
6}

Everything inside the braces is a child, drawn in the order you wrote it, top to bottom. That block of braces is a — the same trailing-lambda syntax from Lesson 1.17 — and Compose gives it a special that unlocks a few extra modifiers. More on that in a moment.

A Column takes two arrangement parameters:

  • verticalArrangement — how the children are spread out along the column.
  • horizontalAlignment — where each child sits across the column.
kotlin
1Column(
2    modifier = Modifier.fillMaxSize(),
3    verticalArrangement = Arrangement.Center,
4    horizontalAlignment =
5        Alignment.CenterHorizontally
6) {
7    Text(text = "DICE DUEL")
8    Text(text = "First to 30 wins")
9}

That is dead centre of the screen, both ways. Note the two different words: Arrangement for the direction the container runs in, Alignment for the other direction. Getting them the wrong way round is the most common mistake in this lesson, and the compiler will catch it.

The values worth knowing:

verticalArrangementEffect
Arrangement.Top (default)All children packed at the top
Arrangement.CenterPacked in the middle
Arrangement.BottomPacked at the bottom
Arrangement.spacedBy(12.dp)Packed at the top with a 12dp gap between each
Arrangement.SpaceBetweenFirst at the top, last at the bottom, gaps shared out
Arrangement.SpaceEvenlyEqual gaps everywhere, including the ends

Arrangement.spacedBy is the one you will reach for most. It puts a gap between children and not at the ends, which is nearly always what you want.

Row: the same idea, sideways

Row is Column rotated. The parameters swap over:

  • horizontalArrangement — spread along the row.
  • verticalAlignment — where each child sits across it.
kotlin
1Row(
2    modifier = Modifier.fillMaxWidth(),
3    horizontalArrangement =
4        Arrangement.spacedBy(14.dp),
5    verticalAlignment = Alignment.CenterVertically
6) {
7    Text(text = "Turn:")
8    Text(text = "PLAYER 1")
9}

Ask a Row for verticalArrangement and the build fails with Cannot find a parameter with this name — a Row does not have one, because vertical is not the direction it runs in.

weight: sharing out the space that is left

Inside a Row or a Column, and only there, every child can use Modifier.weight().

Weight means: after the fixed-size children have taken what they need, share the leftover space between the weighted ones, in proportion.

kotlin
1Row(modifier = Modifier.fillMaxWidth()) {
2    Text(
3        text = "left",
4        modifier = Modifier.weight(1f)
5    )
6    Text(
7        text = "right",
8        modifier = Modifier.weight(1f)
9    )
10}

Two equal halves, whatever the phone's width. Change one to weight(2f) and it takes two thirds.

weight is a scoped modifier: it exists on the modifier you pass to a direct child of a Row or Column, and nowhere else. Use it anywhere else and you get Unresolved reference: weight, which is confusing until you know why. It has to work that way — "share the leftover space" is meaningless without a row or column to have leftover space in.

The related trick is a weighted Spacer:

kotlin
1Column(modifier = Modifier.fillMaxSize()) {
2    TitleBlock()
3    Spacer(Modifier.weight(1f))
4    DieFace(value = 5)
5    Spacer(Modifier.weight(1f))
6    TurnHint()
7}

A Spacer is an empty composable whose only job is to take up room. Two of them with equal weights push the die to the visual centre while the title stays pinned to the top and the hint to the bottom. That is exactly how Dice Duel's game screen is laid out.

A real layout, built from both

Here is Dice Duel's scoreboard: a Row of two Columns.

ScoreBoard.ktkotlin
1@Composable
2fun ScoreBoard(p1: Int, p2: Int) {
3    Row(
4        modifier = Modifier.fillMaxWidth(),
5        horizontalArrangement =
6            Arrangement.spacedBy(14.dp)
7    ) {
8        PlayerPanel(
9            name = "PLAYER 1",
10            score = p1,
11            panel = Color(0xFFFFD9E2),
12            ink = Color(0xFF5F0F27),
13            modifier = Modifier.weight(1f)
14        )
15        PlayerPanel(
16            name = "PLAYER 2",
17            score = p2,
18            panel = Color(0xFFB7F1E6),
19            ink = Color(0xFF00352C),
20            modifier = Modifier.weight(1f)
21        )
22    }
23}
24
25@Composable
26fun PlayerPanel(
27    name: String,
28    score: Int,
29    panel: Color,
30    ink: Color,
31    modifier: Modifier = Modifier
32) {
33    Column(
34        modifier = modifier
35            .clip(RoundedCornerShape(24.dp))
36            .background(panel)
37            .padding(vertical = 16.dp),
38        horizontalAlignment =
39            Alignment.CenterHorizontally
40    ) {
41        Text(text = name, color = ink)
42        Text(text = "$score", color = ink)
43    }
44}
9:41▲ ▮
PLAYER 1
12
PLAYER 2
9

ScoreBoard(p1 = 12, p2 = 9). A Row of two weighted Columns.

Box: layers, and one thing on top of another

A Box draws its children in order, each one on top of the last. The first child is at the back.

Its own parameter is contentAlignment, which positions all children at once:

kotlin
1Box(
2    modifier = Modifier.fillMaxSize(),
3    contentAlignment = Alignment.Center
4) {
5    Text(text = "PLAYER 1 WINS")
6}

Box plus fillMaxSize plus contentAlignment = Alignment.Center is the standard way to centre one thing on a whole screen, and you will write it many times.

Inside a Box, each child can also override the shared alignment with Modifier.align():

kotlin
1Box(modifier = Modifier.fillMaxSize()) {
2    GameScreen()
3    Text(
4        text = "v1.0",
5        modifier = Modifier.align(Alignment.BottomEnd)
6    )
7}

align is another scoped modifier — inside a Box it takes a full Alignment such as Alignment.TopStart or Alignment.BottomEnd; inside a Column it takes only a horizontal one, and inside a Row only a vertical one. The type system enforces it, so you cannot get it wrong for long.

That layering is how Dice Duel shows its win banner: the whole game stays exactly where it is, and a Box puts a dimmed panel over the top of it.

Tip

Start and End, not Left and Right. Android supports languages written right to left, and in Arabic or Hebrew Start becomes the right-hand edge automatically. Using Start/End everywhere costs nothing and means your app is not broken for a billion people.

Try it in Pocket Studio
  1. Open ComposeLab in Pocket Studio.
  2. In MainActivity.kt, put two Text composables directly inside setContent { }, with no container. Tap Run and look closely — they are drawn on top of each other.
  3. Wrap them in Column { }. Run again. Now they stack.
  4. Change Column to Row. Run. Now they sit side by side.
  5. Change it to Box. Run. They overlap again — a Box is what you had at the start, because setContent behaves like one.
  6. Go back to Row and add modifier = Modifier.fillMaxWidth() plus horizontalArrangement = Arrangement.SpaceBetween. Run: one text pinned to each edge.
  7. Now type the ScoreBoard and PlayerPanel code from this lesson, call ScoreBoard(p1 = 12, p2 = 9) from setContent, and Run. You have just built a real piece of Dice Duel.
Error Doctor5 common errors
e: Cannot find a parameter with this name: verticalArrangement
MeansYou used a Column parameter on a Row. A Row runs horizontally, so it has horizontalArrangement and verticalAlignment — never the other pair.
FixRemember the rule: arrangement runs the way the container runs. Column → verticalArrangement + horizontalAlignment. Row → horizontalArrangement + verticalAlignment.
e: Type mismatch: inferred type is Alignment.Horizontal but Alignment.Vertical was expected
MeansYou passed Alignment.CenterHorizontally where a vertical alignment belongs — usually verticalAlignment on a Row.
FixUse Alignment.CenterVertically for a Row's verticalAlignment, and Alignment.CenterHorizontally for a Column's horizontalAlignment. In a Box, use the plain Alignment.Center.
e: Unresolved reference: weight
Meansweight only exists on a direct child of a Row or a Column. Outside one it genuinely does not exist, so Kotlin cannot find it.
FixCheck the composable really is a direct child. A common trap: extracting the child into its own composable and calling Modifier.weight(1f) inside it — by then you are no longer in the Row's scope. Pass the weight in from the caller as part of modifier instead.
e: Unresolved reference: Arrangement
MeansMissing import. Arrangement and Alignment live in different packages, which catches everyone once.
Fiximport androidx.compose.foundation.layout.Arrangement and import androidx.compose.ui.Alignment.
Everything is drawn on top of everything else in the corner
MeansYour composables have no container, so nothing has been told where to go. setContent behaves like a Box: children stack at the top-start.
FixWrap them in a Column or a Row. If you meant them to overlap, wrap them in a Box on purpose — it makes the intent obvious to the next person reading it.
Recap
  • Column stacks downwards, Row runs across, Box layers children on top of each other.
  • Arrangement runs the way the container runs; alignment is the other direction. Column takes verticalArrangement + horizontalAlignment; Row takes horizontalArrangement + verticalAlignment; Box takes contentAlignment.
  • Arrangement.spacedBy(12.dp) is the clean way to put gaps between children.
  • Modifier.weight() shares out leftover space, and only exists inside a Row or Column . A weighted Spacer pushes things apart.
  • Prefer Start/End to Left/Right so right-to-left languages work for free.
  • Next: the heart of Compose. A counter that refuses to count, and the two words that fix it: remember and mutableStateOf.