Column, Row and Box
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?
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:
| Container | Puts its children |
|---|---|
Column | one below the other |
Row | one beside the other |
Box | one 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 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
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.
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:
verticalArrangement | Effect |
|---|---|
Arrangement.Top (default) | All children packed at the top |
Arrangement.Center | Packed in the middle |
Arrangement.Bottom | Packed at the bottom |
Arrangement.spacedBy(12.dp) | Packed at the top with a 12dp gap between each |
Arrangement.SpaceBetween | First at the top, last at the bottom, gaps shared out |
Arrangement.SpaceEvenly | Equal 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.
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.
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:
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.
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}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:
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():
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.
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.
- Open ComposeLab in Pocket Studio.
- In
MainActivity.kt, put twoTextcomposables directly insidesetContent { }, with no container. Tap Run and look closely — they are drawn on top of each other. - Wrap them in
Column { }. Run again. Now they stack. - Change
ColumntoRow. Run. Now they sit side by side. - Change it to
Box. Run. They overlap again — aBoxis what you had at the start, becausesetContentbehaves like one. - Go back to
Rowand addmodifier = Modifier.fillMaxWidth()plushorizontalArrangement = Arrangement.SpaceBetween. Run: one text pinned to each edge. - Now type the
ScoreBoardandPlayerPanelcode from this lesson, callScoreBoard(p1 = 12, p2 = 9)fromsetContent, and Run. You have just built a real piece of Dice Duel.
Column parameter on a Row. A Row runs horizontally, so it has horizontalArrangement and verticalAlignment — never the other pair.verticalArrangement + horizontalAlignment. Row → horizontalArrangement + verticalAlignment.Alignment.CenterHorizontally where a vertical alignment belongs — usually verticalAlignment on a Row.Alignment.CenterVertically for a Row's verticalAlignment, and Alignment.CenterHorizontally for a Column's horizontalAlignment. In a Box, use the plain Alignment.Center.weight only exists on a direct child of a Row or a Column. Outside one it genuinely does not exist, so Kotlin cannot find it.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.Arrangement and Alignment live in different packages, which catches everyone once.import androidx.compose.foundation.layout.Arrangement and import androidx.compose.ui.Alignment.setContent behaves like a Box: children stack at the top-start.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.Columnstacks downwards,Rowruns across,Boxlayers children on top of each other.- Arrangement runs the way the container runs; alignment is the other direction. Column takes
verticalArrangement+horizontalAlignment; Row takeshorizontalArrangement+verticalAlignment; Box takescontentAlignment. 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 weightedSpacerpushes things apart.- Prefer
Start/EndtoLeft/Rightso 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:
rememberandmutableStateOf.