Phase 1 · Getting Started with ComposeModule 12~45 min read

Row, Column & Box: Constraints in Practice

Understand measurement, weight, alignment, stacking and scrolling, then compose a reusable layout that handles long content and larger fonts.

Goals & prerequisites

A layout should explain where content belongs and what happens when it grows. Learn to reason about Row, Column and Box instead of adding spacers until one screenshot looks right.

Complete Layouts: Column, Row & Box first. You need composable functions, modifiers and named parameters. This lesson keeps data static so you can concentrate on measurement, available space and the relationship between a parent and its direct children.

  • Choose a container by its axis and content relationship.
  • Allocate flexible space with scoped weights.
  • Preserve readable content as width and font settings change.
  • Recognize when a scrolling column should become a lazy list.

Think in constraints

A parent supplies a range of allowed widths and heights. A child measures within that context and reports a size; the parent places it. Therefore size(100.dp) expresses a requested size, not a promise that every ancestor must accommodate it. A narrow parent can constrain a child even when the child requests more room.

Before changing a modifier, state the intended rule in words: “This title takes the remaining width, and the duration keeps its natural width.” That sentence is much more useful than “move the label 40 dp to the right.” Rules survive longer titles and different devices.

Choose the container

ComposableChild relationshipTypical job
ColumnOne vertical sequenceA form, details section or short screen
RowOne horizontal sequenceA thumbnail, flexible title and trailing duration
BoxChildren share an area and may overlapA badge positioned over a preview
SpacerAn intentionally empty childA fixed gap or a weighted remainder

Nested containers express structure. A row can contain an image and a column of title plus subtitle. The column is one child of the row, so a weight on that column allocates width to the whole text group. Avoid flattening everything into unrelated coordinates.

Arrangement & alignment

Arrangement distributes children along a container's main axis: horizontal in a Row, vertical in a Column. Alignment works across that axis. SpaceBetween needs spare space to distribute; it does not create width or height by itself.

LayoutAxes.kt
// Inside a @Composable function:
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically,
) {
    Text("Your progress")
    Text("3 of 8")
}

Column(
    verticalArrangement = Arrangement.spacedBy(8.dp),
    horizontalAlignment = Alignment.Start,
) {
    Text("Compose foundations")
    Text("One focused topic at a time")
}
Focused fragments. Use the layout, material3, runtime, Alignment, Modifier and dp imports from the complete example below.

Arrangement.spacedBy defines a repeatable gap between siblings without inserting a Spacer after every child. Use a spacer when the empty area itself has a distinct job. For text-heavy rows, top alignment often reads more naturally than vertically centering every element against a multi-line title.

Weight & scope

A weighted child receives a proportional share of the space left after non-weighted children and spacing are measured. In a bounded row, a title with weight(1f) can wrap while a short duration remains visible. Two equally weighted children share the remaining width equally; weights of two and one produce a two-to-one split of that remainder.

RemainingSpace.kt
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
    // Each weighted child receives a share of the remaining width.
    Text("A longer lesson title can wrap here", Modifier.weight(1f))
    Text("25 min")
}

// For proportional panels, both siblings can be weighted:
Row(Modifier.fillMaxWidth()) {
    Box(Modifier.weight(2f)) { Text("Main content") }
    Box(Modifier.weight(1f)) { Text("Aside") }
}
weight is supplied by RowScope or ColumnScope. Use it on the parent's direct child; do not add an unrelated top-level weight import.

Scope communicates ownership

A modifier created inside RowScope can be passed elsewhere, but the row only interprets weight information on its direct children. Putting the weighted modifier on a grandchild inside a plain Box does not weight that outer box.

Box & overlays

A Box is useful when elements occupy the same region. Later children normally draw over earlier ones. contentAlignment gives the box a default alignment, while Modifier.align on a direct child chooses that child's position within the box.

Overlay.kt
Box(
    modifier = Modifier
        .fillMaxWidth()
        .height(140.dp)
        .background(MaterialTheme.colorScheme.secondaryContainer),
) {
    Text("Lesson preview", modifier = Modifier.align(Alignment.Center))
    Text(
        "Available offline",
        modifier = Modifier.align(Alignment.BottomEnd).padding(12.dp),
        style = MaterialTheme.typography.labelMedium,
    )
}
Focused visual example. Add foundation.layout.height to the complete example's imports to try it.

Overlays need room too. An edge badge can cover the most important part of an image or collide with a long heading. Reserve space when information must remain readable, and check the accessibility reading order separately from the visual stacking order.

matchParentSize lets a direct child follow the box's final size without participating in the size decision. This is useful for a decorative layer behind naturally sized content. In contrast, fillMaxSize asks for the available space and can make the box expand. Neither modifier means “use the size of the screen” independently of the parent.

MatchingDecoration.kt
// Additional import: androidx.compose.foundation.layout.Spacer
// Inside a @Composable function:
Box {
    // This decoration follows the final size; it does not make the Box larger.
    Spacer(
        Modifier
            .matchParentSize()
            .background(MaterialTheme.colorScheme.secondaryContainer),
    )
    Text("Content determines this panel's size", Modifier.padding(16.dp))
}
matchParentSize is provided by BoxScope; Spacer is intentionally empty content used as a background layer.

Modifier boundaries

Read chains from the outside toward the content. padding(16.dp).background(color) leaves that padding outside the painted region; background(color).padding(16.dp) includes the inner padding in the background. Neither chain is universally correct. Choose the one that expresses the intended boundary.

Reusable composables should accept modifier: Modifier = Modifier and apply it once at their root. Start fresh internal chains with Modifier. Reusing the caller's modifier on every child can accidentally repeat external padding, sizing or interaction behavior.

Build a lesson overview

Create LessonOverview.kt beside your generated MainActivity. Retain the package declaration, then paste the imports and code below. Call LessonOverview() inside the generated theme in your existing setContent. The teaching copy is fixed English; move it into string resources when adapting this screen.

LessonOverview.kt
// Keep your generated package declaration above the imports.
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun LessonOverview(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .fillMaxWidth()
            .verticalScroll(rememberScrollState())
            .padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp),
    ) {
        Text("Your learning plan", style = MaterialTheme.typography.headlineSmall)
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .background(MaterialTheme.colorScheme.secondaryContainer)
                .padding(16.dp),
        ) {
            Text(
                "Learn one idea, predict the result, then build it.",
                style = MaterialTheme.typography.bodyLarge,
                color = MaterialTheme.colorScheme.onSecondaryContainer,
            )
        }
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(12.dp),
            verticalAlignment = Alignment.Top,
        ) {
            Column(
                modifier = Modifier.weight(1f),
                verticalArrangement = Arrangement.spacedBy(4.dp),
            ) {
                Text("Layouts that adapt to content", style = MaterialTheme.typography.titleMedium)
                Text("Column, Row, Box and constraints", style = MaterialTheme.typography.bodyMedium)
            }
            Text("25 min", style = MaterialTheme.typography.labelLarge)
        }
        Text(
            "Try a longer title and a larger system font. The title should grow vertically while the duration remains visible.",
            style = MaterialTheme.typography.bodyLarge,
        )
    }
}
Complete worked composable using the standard Compose Material 3 template. A parent screen must supply bounded visible height for scrolling.

The overview should show a painted message panel, then a two-part row. The text group owns the row's remaining width, and the duration keeps its natural width. Increase the title length: its column should become taller instead of pushing the duration beyond the right edge.

Scrolling & growing content

A regular Column composes its children eagerly. Adding verticalScroll(rememberScrollState()) makes a short collection of content reachable when it exceeds the viewport. A long or unbounded dataset belongs in a lazy container such as LazyColumn, which creates the needed items as the user scrolls.

Do not use vertical weight to fill “remaining height” inside content measured with unbounded scroll height: there is no finite remainder to divide. Also avoid nesting an unconstrained vertical lazy list inside a vertically scrolling column. Prefer one scrolling owner and put the header, rows and footer into that container.

Debug the layout

ProblemReason to investigateCorrection to test
SpaceBetween has no visible effectThe parent wraps its childrenGive the intended axis available space before distributing it.
A trailing label falls offscreenThe title consumed the row's widthWeight the direct text-group child and keep the label unweighted.
Large fonts overlap a footerA fixed height assumes one text sizeAllow natural height and make overflow reachable.
Padding doubles unexpectedlyThe external modifier was applied repeatedlyApply the caller modifier once to the root.

Use Android Studio's Layout Inspector when it is unclear which parent owns the extra space. Test a narrow window and the largest practical font setting. If the duration itself becomes too wide, a different structure, such as stacking it below the title, can express the content better than more weight adjustments.

Practice & worked solution

  1. Replace the title with a sentence twice as long. Verify the duration remains visible.
  2. Reverse background and padding in the message panel. Explain exactly which area changes color.
  3. Adapt the title/duration section to stack vertically when its local available width is below 360 dp. Treat this as a teaching threshold, then inspect whether your actual content needs a different rule.

Acceptance checks: both arrangements show the complete title and duration, there is no horizontal scrolling, and growing text remains reachable. Change the emulator's window width rather than assuming portrait and landscape are reliable proxies for available space.

Reveal one responsive solution

BoxWithConstraints exposes the local bounds for this section. Keep the content identical in both branches; only the placement rule changes. For a full app, continue to the course's adaptive layout lesson.

LessonOverview-adaptive.kt
// Add: import androidx.compose.foundation.layout.BoxWithConstraints
// Add: import androidx.compose.foundation.layout.ColumnScope
// Replace the Row in LessonOverview with:
BoxWithConstraints(Modifier.fillMaxWidth()) {
    // This 360 dp threshold is a local teaching choice, not a device category.
    if (maxWidth < 360.dp) {
        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            LessonDetails()
            Text("25 min", style = MaterialTheme.typography.labelLarge)
        }
    } else {
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            Column(Modifier.weight(1f)) { LessonDetails() }
            Text("25 min", style = MaterialTheme.typography.labelLarge)
        }
    }
}

// Add this top-level helper; ColumnScope makes the intended caller explicit.
@Composable
private fun ColumnScope.LessonDetails() {
    Text("Layouts that adapt to content", style = MaterialTheme.typography.titleMedium)
    Text("Column, Row, Box and constraints", style = MaterialTheme.typography.bodyMedium)
}
Patch to the worked example. ColumnScope is an explicit contract for the text helper.

Quick check & references

Key takeaways

  • Describe the relationship between content before choosing modifiers.
  • Arrangement follows the main axis; alignment follows the cross axis.
  • Weights divide remaining bounded space and belong on direct Row or Column children.
  • Use natural text height, one scrolling owner and content-aware adaptation.

Quick check

1. A title and a trailing duration share a row. Which child should usually take weight?

2. Why might vertical weight fail inside a vertically scrolling Column?

3. Which change makes a short form accessible when larger fonts exceed the visible height?

Read the official guides to layout basics, modifiers and scope, scroll modifiers, and lazy lists. Continue to Adaptive Layouts & Accessibility for app-level decisions.