RizTech Academy logo
RizTech Academy
Layout and ThemingLesson 1 of 730 min

Row, Column and Box

Module 3's PriceCard works, but parts of it work by accident. A Spacer makes the gap before the price, "₹2,450 / quintal" is one string in one style, so the unit shouts as loudly as the number, and the range is a sentence the eye has to parse. Compose has three basic layouts, Column, Row and Box, and almost every screen you will build is those three nested. This lesson explains how they measure their children, runs the one Row bug everybody meets, and rebuilds the card on purpose.

How Compose measures a layout

Every layout goes through the same three steps, once per frame that needs it:

  1. The parent passes constraints down: a minimum and maximum width and height. "You may be anything from 0 to 379 dp wide."
  2. Each child measures itself within those constraints and reports its size.
  3. The parent places each child at an x and y position.

Each child is measured once. A layout that tries to measure the same child twice fails at run time, which is why Compose can nest Row inside Column inside Row without the cost that made XML developers flatten everything into one ConstraintLayout. Deep nesting in Compose is normal and cheap. That changes how you design: you reach for a small nested layout first, and lesson 3 shows when anything heavier earns its place.

Column and Row

A Column places children top to bottom, a Row left to right. Each has two settings: how children are spread along its direction (arrangement), and how they line up across it (alignment).

Column(
    verticalArrangement = Arrangement.spacedBy(4.dp),
    horizontalAlignment = Alignment.Start,
) { /* ... */ }

Row(
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically,
) { /* ... */ }
Arrangement What it does with spare space
Arrangement.Top / Start (default) Packs children at the start; spare space at the end
Arrangement.Bottom / End Packs children at the end
Arrangement.Center Packs children in the middle
Arrangement.SpaceBetween First at the start, last at the end, equal gaps between
Arrangement.SpaceAround Equal space round each child, so half-gaps at the edges
Arrangement.SpaceEvenly Equal gaps everywhere, edges included
Arrangement.spacedBy(8.dp) A fixed gap between children, none at the edges
Layout Alignment across Values
Column horizontalAlignment Alignment.Start, CenterHorizontally, End
Row verticalAlignment Alignment.Top, CenterVertically, Bottom

A single child can override the alignment with Modifier.align(…), but only with the type its parent accepts. In a Column, Modifier.align(Alignment.TopEnd) does not compile:

Argument type mismatch: actual type is 'Alignment', but 'Alignment.Horizontal' was expected.

A Column only aligns horizontally, so it only accepts a horizontal alignment.

spacedBy is the one you will use most. It replaces a Spacer between every pair of children, and unlike padding on each child it puts no gap before the first or after the last.

Baselines

When two texts of different sizes sit in a Row, centring them looks wrong: the small one floats above the bottom of the large one. What the eye wants is for the letters to sit on the same line, the baseline. Modifier.alignByBaseline() on each child does that. It exists only inside a Row, for the same scope reason as weight.

The trap: a Row never wraps

weight(1f) from module 3 shares out the space left over once the other children are measured. What happens without it is worth seeing once. On the emulator, in a Row 240 dp wide:

Row(modifier = Modifier.width(240.dp).border(1.dp, MaterialTheme.colorScheme.outline)) {
    Text(text = "Vashi APMC, Navi Mumbai, Sector 19")
    Text(text = "₹2,100")
}

The market name filled all 240 dp and wrapped onto two lines inside the border. The price was printed down the right-hand side, outside the border, one character per line: ₹, 2, ,, 1, 0, 0. The accessibility tree did not list the price at all.

Read it with the three steps. The Row measured the market name first and offered it the full 240 dp; the name was longer than that, so it took everything. The price was measured second with no width left, and a Text squeezed that hard still draws its characters, one per line, wherever they land. A Row never moves a child to the next line. Nothing crashed, no warning appeared, and with a short market name such as "Nashik APMC" it looks fine, which is how it ships.

The fix is one modifier:

Row(modifier = Modifier.width(240.dp).border(1.dp, MaterialTheme.colorScheme.outline)) {
    Text(text = "Vashi APMC, Navi Mumbai, Sector 19", modifier = Modifier.weight(1f))
    Text(text = "₹2,100")
}

Weighted children are measured last. The price is measured first and takes its natural 51 dp; the name gets the 189 dp left and wraps inside it. The rule that comes out of this: in a Row, the child that can wrap gets the weight; the child that must stay whole, such as a price, does not.

Box

A Box stacks its children on top of each other, the last one drawn on top. Use it for overlays: a badge on a card, a label on an image, a loading spinner over content.

Box(modifier = Modifier.fillMaxWidth()) {
    PriceCard(price = SamplePrices.puneOnion, modifier = Modifier.fillMaxWidth())
    Text(
        text = "Lowest",
        style = MaterialTheme.typography.labelLarge,
        modifier = Modifier
            .align(Alignment.TopEnd)
            .padding(12.dp)
            .background(MaterialTheme.colorScheme.tertiaryContainer, RoundedCornerShape(50))
            .padding(horizontal = 12.dp, vertical = 4.dp),
    )
}

Inside a Box, Modifier.align takes a full two-way Alignment: TopStart, TopCenter, TopEnd, CenterStart, Center, CenterEnd, BottomStart, BottomCenter, BottomEnd. contentAlignment on the Box sets the default for every child. Modifier.matchParentSize() makes a child exactly the Box's size without affecting it, which is how you put a tinted scrim over content.

What a Box is not for: placing things side by side. The children do not know about each other. On the emulator the "Lowest" badge sat neatly in the corner of the Pune onion card. Give that card a commodity of "Onion (red, Nashik variety)" and the title runs underneath the badge, because the Box never told the title the badge was there. If two things must not overlap, they belong in a Row.

Rebuilding PriceCard on purpose

Replace the body of PriceCard in ui/PriceCard.kt (the previews stay as they are):

@Composable
fun PriceCard(price: CommodityPrice, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {
        Column(
            modifier = Modifier.padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            Text(
                text = price.commodity,
                style = MaterialTheme.typography.titleLarge,
            )
            Text(
                text = "${price.market} · ${priceDateFormat.format(price.date)}",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
            )
            Row(
                modifier = Modifier.padding(top = 8.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
            ) {
                Text(
                    text = formatRupees(price.modalPrice),
                    style = MaterialTheme.typography.headlineSmall,
                    modifier = Modifier.alignByBaseline(),
                )
                Text(
                    text = "per quintal",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                    modifier = Modifier.alignByBaseline(),
                )
            }
            Row(modifier = Modifier.fillMaxWidth()) {
                Text(
                    text = "Min ${formatRupees(price.minPrice)}",
                    style = MaterialTheme.typography.bodyMedium,
                    modifier = Modifier.weight(1f),
                )
                Text(
                    text = "Max ${formatRupees(price.maxPrice)}",
                    style = MaterialTheme.typography.bodyMedium,
                )
            }
        }
    }
}

Fix the imports: add Arrangement, Row and fillMaxWidth from androidx.compose.foundation.layout, and remove Spacer and height, which are no longer used. Every decision is one from this lesson:

  • spacedBy(4.dp) puts the same small gap between every line, and padding(top = 8.dp) on the price row adds a larger one where the card changes from "what and where" to "how much".
  • The modal price and its unit are two texts in a Row, baseline-aligned, so the number is large and the unit is quiet. spacedBy(6.dp) separates them.
  • Min and max are a Row with weight(1f) on the first, so min sits at the left edge and max at the right, whatever their widths. The weight goes on the one that can give way.

The market and date are still one line. Module 3 found that this line breaks badly at a large font size; lesson 7 reproduces that on the emulator and fixes it, so leave it for now.

Run the app. Each card now reads top to bottom as commodity, place and date, then a large ₹2,450 with a smaller "per quintal" sitting on the same baseline, then "Min ₹1,200" at the left and "Max ₹2,851" at the right.

What the rework costs: the card is now six composables where it was five, and two Rows deeper. In Compose that is nothing measurable. What it does cost is a little reading time for the next person; the structure pays for itself because each line says what it is for.

Check your work

The three steps of a Compose layout. Constraints go down, sizes come up, the parent places.

Why deep nesting is fine in Compose. Each child is measured once per layout pass, so nesting does not multiply measurement the way nested XML layouts could.

The difference between spacedBy(8.dp) and SpaceEvenly. spacedBy puts a fixed gap only between children; SpaceEvenly divides all the spare space equally, edges included.

Why the price printed one character per line. The Row measured the long name first and it took the full width, leaving the price no width at all.

Where the weight goes in a label-and-value row. On the child that can wrap or shrink, never on the value that must stay whole.

Why Modifier.align(Alignment.TopEnd) fails in a Column. A Column only aligns children horizontally, so its align takes an Alignment.Horizontal.

What a Box does not do. It does not keep its children from overlapping.

Practice 2. With verticalAlignment = Alignment.CenterVertically the two texts are centred on each other, so the small "per quintal" sits higher than the bottom of the large price and the letters no longer share a line. Baseline alignment is what text needs.

Practice 4. Put the badge and the title in a Row instead: the title with weight(1f), the badge after it. The title then wraps before it reaches the badge.

Practice 6. One answer:

@Composable
fun PriceStat(label: String, amount: Long, modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Text(text = label, style = MaterialTheme.typography.labelMedium)
        Text(text = formatRupees(amount), style = MaterialTheme.typography.titleMedium)
    }
}

Row(modifier = Modifier.fillMaxWidth()) {
    PriceStat("Min", price.minPrice, Modifier.weight(1f))
    PriceStat("Modal", price.modalPrice, Modifier.weight(1f))
    PriceStat("Max", price.maxPrice, Modifier.weight(1f))
}

Equal weights split the whole width into three equal columns, because nothing in the Row is unweighted; content width does not matter. On a 320 dp screen each column gets under 90 dp once the screen and card padding are taken off. At 1.5× a value too wide for its column wraps inside that column; it never pushes the other two aside. That is the opposite of the trap above, and the reason weights are the right tool here.

Practice

  1. Rebuild PriceCard as above and run it. Compare it with a screenshot of the module 3 card.
  2. Replace the two alignByBaseline() calls with verticalAlignment = Alignment.CenterVertically on the Row. Describe what changed, then put the baselines back.
  3. Reproduce the Row trap in a preview with widthDp = 240, then fix it with weight.
  4. Add the "Lowest" badge from the Box example over one card. Then change that card's commodity to "Onion (red, Nashik variety)" in the preview and watch the overlap. Fix it without a Box.
  5. Break it on purpose: put Modifier.align(Alignment.TopEnd) on a Text inside a Column and read the compiler's message. Say what type it wanted and why.
  6. Harder: write PriceStat(label: String, amount: Long, modifier: Modifier = Modifier) and use three of them in a Row so min, modal and max get equal columns. Check it in the large-text preview and say what happens to a column whose content is too wide.

Official documentation

  • Compose layout basics — The measurement model, the standard layouts and the single-measurement rule.
  • Alignment lines in Compose — How baselines work, and how to define your own alignment line.
  • Box — Every Box overload and BoxScope modifier, including matchParentSize.

Next: edge-to-edge, and what the app must do now that it draws under the system bars.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship