Techbase Academy
Android · Lesson 38

Layouts and Modifiers in Depth

11 phasesRecipe Detail ScreenAndroid Studio
Phase 1 of 11
Lesson Introduction

Almost every Compose problem that makes beginners say "why does it look like that?" is a layout problem: a button stretches to the full width, padding appears on the wrong side, a background paints outside the border, text is cut off on a small phone. The cure is to understand how Compose measures and places things, and to learn the handful of building blocks that do 95% of the work.

This lesson covers the layout parts of the Jetpack Compose for Android developers programme: The Compose UI toolkit, Implement a real-world design, Fundamentals of Compose layouts and modifiers and Constraints and modifier order (Pathways 1 and 2, https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1). You will practise on a recipe screen of your own design.

By the end you will be able to:

  • Use Row, Column and Box with Arrangement and Alignment.
  • Share space with weight, fillMaxWidth and fractions.
  • Predict the effect of modifier order.
  • Explain how constraints flow down and sizes flow up.
  • Use Scaffold, Surface, Card and Spacer properly.
  • Use ConstraintLayout in Compose when nesting gets awkward.
  • Use Modifier.layout and intrinsic sizes in simple cases.
  • Turn a visual design into composables, and handle small screens, large fonts and right-to-left languages.

Big idea: a parent tells each child "you may be between this small and this big"; the child picks a size inside that range; then the parent decides where to put it.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 11
Prerequisite Concepts
  • The three phases and Compose basics (lesson 37). Layout is phase two.
  • Row, Column, Modifier first steps (lesson 16).
  • dp and sp units (lesson 16). dp for sizes and spacing, sp for text.
  • Preview (lesson 15). Use previews with widthDp, fontScale and locale options throughout.
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 11
Conceptual Understanding: How Layout Works

Constraints down, sizes up

text
        parent
          |  constraints: minWidth..maxWidth, minHeight..maxHeight
          v
        child  --- measures itself (and its own children) ---+
          |                                                  |
          +---------------- reports a size (w x h) <---------+
        parent places the child at (x, y)

A Constraints object holds a minimum and maximum width and height, in pixels. A child must pick a size within them. This has surprising, useful consequences:

  • A composable cannot be bigger than its parent allows, unless the parent allows it.
  • Modifier.size(300.dp) inside a parent that is only 200 dp wide yields 200 dp, because the constraint wins (you can override with requiredSize, which you rarely should).
  • Each child is measured once, which is why deep nesting is cheap in Compose compared with old Views.

Modifier order matters

A Modifier is an ordered chain. Each element wraps what comes after it, like layers of an onion or nested boxes, from the outside in. So the same elements in a different order give a different result.

kotlin
// A: background covers padding area
Text("Hello", Modifier.background(Color.Yellow).padding(16.dp))
// B: padding is OUTSIDE the background
Text("Hello", Modifier.padding(16.dp).background(Color.Yellow))
text
A: background then padding            B: padding then background
+---------------------+               (transparent margin)
|  yellow all over    |                  +-------+
|     Hello           |                  |yellow |
+---------------------+                  | Hello |
(padding is INSIDE yellow)               +-------+

In A the yellow fills the text and the padding. In B the padding is empty space outside the yellow box. The same applies to clickable: Modifier.padding(16.dp).clickable { } makes only the inner area clickable; Modifier.clickable { }.padding(16.dp) makes the padding clickable too, which is usually what you want for a comfortable touch target.

A useful rule for sizing:

kotlin
Box(Modifier.size(100.dp).padding(8.dp).background(Color.Red)) // red box is 84 dp: padding eats from the 100
Box(Modifier.padding(8.dp).size(100.dp).background(Color.Red)) // red box is 100 dp, with 8 dp margin outside

Read left to right: size first means "I am 100 dp, including whatever follows"; padding first means "leave 8 dp around me, then I am 100".

A common order for a clickable card: size or fillMaxWidth first, then clip, background, clickable, then padding for inner spacing. Experiment with each to see why.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: How Layout Works. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 11
Step-by-Step Build 1: The Big Three

Column and Row

Column stacks children vertically; Row stacks horizontally. Each has two axes: the main axis (the direction of stacking) and the cross axis. You control each with a different parameter.

Main axis Cross axis
Column vertical, verticalArrangement horizontal, horizontalAlignment
Row horizontal, horizontalArrangement vertical, verticalAlignment
kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Row(
 modifier = Modifier.fillMaxWidth(),
 horizontalArrangement = Arrangement.SpaceBetween, // main axis: distribute
 verticalAlignment = Alignment.CenterVertically // cross axis: align
) {
 Text("Left")
 Text("Middle")
 Text("Right")
}

Arrangements, for a row with extra space (✱ = child):

text
Arrangement.Start          ✱✱✱.............
Arrangement.Center         ......✱✱✱.......
Arrangement.End            .............✱✱✱
Arrangement.SpaceBetween   ✱......✱......✱
Arrangement.SpaceAround    .✱....✱....✱.   (half-gaps at the ends)
Arrangement.SpaceEvenly    ..✱...✱...✱..   (equal gaps everywhere)
Arrangement.spacedBy(8.dp) ✱ ✱ ✱ ......... (fixed gap, packed at start)

Important: a Row or Column wraps its content by default. Arrangement and Alignment have no visible effect unless the container is bigger than its content: give it fillMaxWidth(), fillMaxSize() or a size. Forgetting this is the most common "why does Arrangement.Center not work?" bug.

Sharing space with weight

Inside a Row or Column, Modifier.weight(w) divides the remaining space among weighted children in proportion:

kotlin
Row(Modifier.fillMaxWidth()) {
 Text("Label", Modifier.weight(1f)) // gets 1/3 of the remaining width
 Text("Value", Modifier.weight(2f)) // gets 2/3
}

Unweighted children are measured first at their natural size; the leftover is then split among the weighted ones. A single weight(1f) child between two fixed-size ones makes it fill the middle (a classic "title between two icons" toolbar). fill = false lets a weighted child take at most its share but shrink if it needs less.

fillMaxWidth(0.5f) is the other option: a fraction of the parent's maximum, not of the leftover. Use weight to share among siblings; use fractions for a single element.

Box: stacking

Box places children on top of each other, like layers. contentAlignment positions them all, and Modifier.align(...) positions one child.

kotlin
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.background
import androidx.compose.ui.graphics.Color
Box(Modifier.size(200.dp).background(Color.LightGray)) {
 Text("Center", Modifier.align(Alignment.Center))
 Text("Top end", Modifier.align(Alignment.TopEnd))
 Text("Bottom start", Modifier.align(Alignment.BottomStart))
}

Use Box for badges on avatars, text over images, loading overlays and a floating button over content. Children are drawn in order, so later ones appear on top. Notice TopEnd and Start: these respect layout direction, flipping in right-to-left languages such as Arabic or Hebrew. Prefer Start and End over Left and Right, and use padding(start = ..., end = ...).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: The Big Three. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 11
Step-by-Step Build 2: Scaffold, Surface, Card and Spacing

Surface and Card

Surface is the basic Material container: it gives a background colour from the theme, a shape, elevation and content colour, and it ensures text on it uses the right "on" colour. Card, ElevatedCard and OutlinedCard are ready-made variants. A rule of thumb: never hand-roll a coloured Box for a panel when Surface or Card exists, because those honour dark theme automatically (lesson 40).

Scaffold

Scaffold implements the standard Material screen skeleton: top bar, bottom bar, floating action button, snackbar host and the main content. It passes you innerPadding so content never hides behind the bars:

kotlin
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipesScreen() {
 Scaffold(
 topBar = { TopAppBar(title = { Text("Recipes") }) },
 floatingActionButton = {
 FloatingActionButton(onClick = { /* add recipe */ }) {
 Icon(Icons.Filled.Add, contentDescription = "Add recipe")
 }
 }
 ) { innerPadding ->
 RecipeList(Modifier.padding(innerPadding))
 }
}

Forgetting to apply innerPadding makes the first list item vanish under the top bar, a bug you will meet often. Since Android 15 enforces edge-to-edge drawing for apps targeting recent API levels, Scaffold and its padding also handle the system bars (status bar and navigation bar), which is another reason to use it (lesson 40 covers system bars).

Spacing

  • Spacer(Modifier.height(16.dp)) adds fixed empty space between siblings.
  • Arrangement.spacedBy(8.dp) on a Column or Row is cleaner than putting a Spacer between every pair.
  • Modifier.padding for space around a composable; contentPadding on lazy lists for space inside the scroll area.

Keep spacing on a scale (4, 8, 12, 16, 24, 32 dp). Consistent numbers make a design look intentional.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Scaffold, Surface, Card and Spacing. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 11
Step-by-Step Build 3: ConstraintLayout in Compose

Most screens are fine with Row, Column and Box. When you need to align things relative to each other in a way that would take three levels of nesting, ConstraintLayout is cleaner. Add its Compose artifact (check the current version at https://developer.android.com/jetpack/androidx/releases/constraintlayout):

libs.versions.toml
[versions]
constraintlayoutCompose = "<latest stable>"
[libraries]
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayoutCompose" }
build.gradle.kts
dependencies {
 implementation(libs.androidx.constraintlayout.compose)
}

A profile header: avatar on the left, name and role stacked to its right, and a follow button pinned to the end, vertically centred with the avatar.

kotlin
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.background
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
@Composable
fun ProfileHeader(name: String, role: String, modifier: Modifier = Modifier) {
 ConstraintLayout(modifier = modifier.fillMaxWidth().padding(16.dp)) {
 val (avatar, title, subtitle, follow) = createRefs()
 Box(
 Modifier
 .size(56.dp)
 .clip(CircleShape)
 .background(MaterialTheme.colorScheme.primary)
 .constrainAs(avatar) {
 start.linkTo(parent.start)
 top.linkTo(parent.top)
 bottom.linkTo(parent.bottom)
 }
 )
 Text(
 name,
 style = MaterialTheme.typography.titleMedium,
 modifier = Modifier.constrainAs(title) {
 start.linkTo(avatar.end, margin = 12.dp)
 top.linkTo(avatar.top)
 }
 )
 Text(
 role,
 modifier = Modifier.constrainAs(subtitle) {
 start.linkTo(title.start)
 top.linkTo(title.bottom)
 }
 )
 Button(
 onClick = { },
 modifier = Modifier.constrainAs(follow) {
 end.linkTo(parent.end)
 top.linkTo(parent.top)
 bottom.linkTo(parent.bottom)
 }
 ) { Text("Follow") }
 }
}

How to read it:

  • createRefs() makes references, one per child; constrainAs(ref) { ... } attaches constraints to a child.
  • start.linkTo(parent.start) means "my start edge sticks to the parent's start edge". Linking both top and bottom to the parent centres the item vertically.
  • margin = 12.dp is spacing from the target.
  • You can also use createGuidelineFromStart(0.3f), createBarrier(...) and createHorizontalChain(...), which have no easy Row/Column equivalent, and Dimension.fillToConstraints to stretch between two anchors.

When to choose it: relative positioning, chains, barriers, guidelines, and responsive designs that rearrange via ConstraintSet. When not to: a simple stack or row (use Column and Row); each tool has a cost, and a Row is easier to read and to review.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: ConstraintLayout in Compose. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
Step-by-Step Build 4: Constraints and Custom Layout

Seeing constraints at work

kotlin
Box(Modifier.size(100.dp).background(Color.Gray)) {
 Box(Modifier.size(300.dp).background(Color.Red)) // ask for 300 dp
}

The inner box asks for 300 dp, but its parent only allows up to 100 dp, so it is clamped to 100. Then change the inner modifier to requiredSize(300.dp): it now ignores the constraint and overflows (it still places at the top-left of its parent). It is rarely the right fix; usually the real fix is to change the parent.

fillMaxWidth() sets the minimum width equal to the maximum width, forcing the child to fill. wrapContentSize() resets the minimum to zero, letting the child be its natural size inside a larger parent, a useful trick when a parent's minimum constraints stretch a child you wanted small.

Modifier.layout

When no existing modifier does what you need, you can intercept measuring and placing:

kotlin
import androidx.compose.ui.layout.layout
fun Modifier.firstBaselineToTop(firstBaselineToTop: Dp) = this.layout { measurable, constraints ->
 val placeable = measurable.measure(constraints)
 val firstBaseline = placeable[FirstBaseline]
 val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
 val height = placeable.height + placeableY
 layout(placeable.width, height) {
 placeable.placeRelative(0, placeableY)
 }
}

The three steps are always: measure the child with measurable.measure(constraints), report a size with layout(width, height), and place it with placeRelative. (Imports: androidx.compose.ui.layout.FirstBaseline, androidx.compose.ui.unit.Dp.) For whole custom containers use the Layout composable with a list of children. This is an advanced tool; use it rarely, and prefer composing existing pieces.

Intrinsic measurements

Children are measured once, so a child cannot normally ask its siblings how big they will be. Intrinsics are a controlled exception: Modifier.height(IntrinsicSize.Min) on a Row makes it as tall as its tallest child's minimum intrinsic height, which lets you draw a divider that matches the text height:

kotlin
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.VerticalDivider
Row(Modifier.height(IntrinsicSize.Min)) {
 Text("Left text", Modifier.weight(1f).padding(8.dp))
 VerticalDivider(Modifier.fillMaxHeight())
 Text("Right text\nwith two lines", Modifier.weight(1f).padding(8.dp))
}

Without IntrinsicSize.Min, the divider's fillMaxHeight would stretch to the whole parent. With it, the row is only as tall as its tallest text. It costs an extra measuring pass, so use it for small, local layouts, not in the middle of long lists.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: Constraints and Custom Layout. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Implement a Real-World Design

The programme's Implement a real-world design item gives you a mockup and asks you to rebuild it. The skill is breaking a picture into a tree of containers. Take this recipe card design:

text
+------------------------------------------+
| [photo, full width, 180 dp tall]         |
|                                          |
+------------------------------------------+
| Jollof Rice with Chicken          [heart]|
| 45 min  ·  4 servings  ·  Easy           |
|                                          |
| A smoky party-style classic that...      |
|                                          |
| [ View recipe ]            [ Share ]     |
+------------------------------------------+

Process:

  1. Outer shape: a Card containing a Column.
  2. Row 1: a Box or image 180 dp tall, fillMaxWidth, ContentScale.Crop.
  3. Row 2: a Row with the title taking weight(1f) and an IconButton at the end.
  4. Row 3: a Row of three Texts separated by dots, or a single formatted Text.
  5. Body: a Text with a max of 3 lines and an ellipsis.
  6. Actions: a Row with Arrangement.SpaceBetween.
  7. Spacing: use 16 dp content padding and 8 dp between rows, via padding and spacedBy.
kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.text.style.TextOverflow
@Composable
fun RecipeCard(
 title: String,
 meta: String,
 summary: String,
 isFavourite: Boolean,
 onToggleFavourite: () -> Unit,
 onViewClick: () -> Unit,
 onShareClick: () -> Unit,
 modifier: Modifier = Modifier
) {
 Card(modifier = modifier.fillMaxWidth()) {
 Column {
 Box(
 Modifier
 .fillMaxWidth()
 .height(180.dp)
 .background(MaterialTheme.colorScheme.secondaryContainer) // stand-in for the photo
 )
 Column(
 modifier = Modifier.padding(16.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 Row(verticalAlignment = Alignment.CenterVertically) {
 Text(
 title,
 style = MaterialTheme.typography.titleLarge,
 modifier = Modifier.weight(1f)
 )
 IconButton(onClick = onToggleFavourite) {
 Icon(
 imageVector = if (isFavourite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
 contentDescription = if (isFavourite) "Remove from favourites" else "Add to favourites"
 )
 }
 }
 Text(meta, style = MaterialTheme.typography.labelLarge)
 Text(summary, maxLines = 3, overflow = TextOverflow.Ellipsis)
 Row(
 modifier = Modifier.fillMaxWidth(),
 horizontalArrangement = Arrangement.SpaceBetween
 ) {
 Button(onClick = onViewClick) { Text("View recipe") }
 OutlinedButton(onClick = onShareClick) { Text("Share") }
 }
 }
 }
 }
}
@Preview(showBackground = true, widthDp = 360)
@Composable
fun RecipeCardPreview() {
 MaterialTheme {
 RecipeCard(
 title = "Jollof Rice with Chicken",
 meta = "45 min  ·  4 servings  ·  Easy",
 summary = "A smoky party-style classic with a rich tomato and pepper base, slow-cooked until the rice is tender and a little crisp at the bottom.",
 isFavourite = true,
 onToggleFavourite = {}, onViewClick = {}, onShareClick = {}
 )
 }
}

Test the design under stress

A design is not finished until it survives real conditions. Add previews for:

kotlin
@Preview(name = "Small phone", widthDp = 320, showBackground = true)
@Preview(name = "Large font", fontScale = 1.8f, widthDp = 360, showBackground = true)
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Preview(name = "Arabic RTL", locale = "ar", widthDp = 360, showBackground = true)
@Composable
fun RecipeCardVariants() { /* same body as above */ }

Check each: Does the title wrap instead of clipping at font scale 1.8? Do the two buttons still fit at 320 dp? Does the heart appear on the correct side in right-to-left? Does text remain readable in dark mode? On a very small screen, two Buttons side by side may not fit; the fix might be FlowRow (which wraps children onto a new line) or stacking them vertically below a width breakpoint (lesson 27).

✏️ Your Task
Practise what you just learned about Implement a Real-World Design. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 11
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Predict the order

For each snippet, draw what you expect (size of the red area, where taps register) before running it.

kotlin
// 1
Box(Modifier.size(120.dp).padding(20.dp).background(Color.Red))
// 2
Box(Modifier.padding(20.dp).size(120.dp).background(Color.Red))
// 3
Box(Modifier.background(Color.Red).padding(20.dp).size(80.dp))
// 4
Box(Modifier.size(80.dp).clickable { }.padding(20.dp).background(Color.Red))
  1. The red area is 80 dp square (120 dp minus 20 dp padding on each side), because size comes first and padding eats from it.
  2. The red area is 120 dp square; 20 dp of empty margin surrounds it, so the whole element occupies 160 dp.
  3. background is first, so the red covers everything inside it: padding (20 dp each side) plus the 80 dp content: 120 dp red square in total.
  4. The element is 80 dp. The click area is the full 80 dp (clickable comes before padding, so it covers the padded region). The red area is the inner 40 dp (80 minus 20 on each side).

Draw them on squared paper to confirm. Remember: each modifier wraps what comes after it.

🎯 Your Challenge

Exercise 2: Build a chat bubble row

Create MessageRow(text, time, isMine). Messages from me sit on the right in a coloured bubble; others on the left in a grey bubble. The bubble is at most 75% of the screen width, with the time at its bottom-right inside. Use Row, Box, weight, widthIn or fillMaxWidth(fraction) appropriately and Start/End for RTL support.

kotlin
import androidx.compose.foundation.layout.Arrangement
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.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun MessageRow(text: String, time: String, isMine: Boolean, modifier: Modifier = Modifier) {
 Row(
 modifier = modifier
 .fillMaxWidth()
 .padding(horizontal = 12.dp, vertical = 4.dp),
 horizontalArrangement = if (isMine) Arrangement.End else Arrangement.Start
 ) {
 Surface(
 shape = RoundedCornerShape(16.dp),
 color = if (isMine) MaterialTheme.colorScheme.primaryContainer
 else MaterialTheme.colorScheme.surfaceVariant,
 modifier = Modifier.fillMaxWidth(0.75f) // at most 75% of the row's width
 ) {
 Column(Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
 Text(text)
 Text(
 time,
 style = MaterialTheme.typography.labelSmall,
 modifier = Modifier.align(Alignment.End)
 )
 }
 }
 }
}

fillMaxWidth(0.75f) forces the bubble to exactly 75% even for short messages, which looks wrong. For "at most 75%, otherwise wrap content" use a width cap from the available space (for example with BoxWithConstraints { Modifier.widthIn(max = maxWidth * 0.75f) }), and the row's Arrangement.End or Start then positions it. That variant is the better answer for a real chat; try it as a follow-up. The use of Alignment.End and horizontalArrangement.End makes the layout flip correctly in right-to-left languages.

🎯 Your Challenge

Exercise 3: ConstraintLayout versus Row

Rebuild ProfileHeader using only Row and Column. Which is shorter? Which is easier to change if the designer asks to move the Follow button below the name on small screens?

kotlin
@Composable
fun ProfileHeaderRow(name: String, role: String, modifier: Modifier = Modifier) {
 Row(
 modifier = modifier.fillMaxWidth().padding(16.dp),
 verticalAlignment = Alignment.CenterVertically
 ) {
 Box(Modifier.size(56.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primary))
 Column(Modifier.weight(1f).padding(horizontal = 12.dp)) {
 Text(name, style = MaterialTheme.typography.titleMedium)
 Text(role)
 }
 Button(onClick = {}) { Text("Follow") }
 }
}

The Row version is shorter and arguably clearer for this particular design. For a small-screen variant where the button moves below, either approach needs a conditional (a different arrangement based on width), but with Row/Column you swap containers simply. ConstraintLayout earns its keep when many items depend on each other irregularly (barriers, chains, guidelines), not for a three-part header. A senior habit: reach for the simplest container that works.

✏️ Your Task

Build the recipe card, then run all four variant previews. List the problems you find (for example a clipped title or overlapping buttons) and fix at least two.

Phase 10 of 11
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Arrangement or Alignment has no effect. Why: the container is exactly as big as its content. The fix: give it fillMaxWidth(), fillMaxSize() or a size.

Mistake #2: Wrong modifier order. Symptoms: padding in the wrong place, a tiny clickable area, a background spilling over a clip. The fix: read the chain from the outside in, and reorder.

Mistake #3: fillMaxHeight inside a scrolling Column. What happens: a crash about infinite maximum height. The fix: give the child a fixed height, or use weight only in non-scrolling parents.

Mistake #4: Ignoring innerPadding in Scaffold. What happens: content hides under bars. The fix: apply Modifier.padding(innerPadding).

Mistake #5: Left and right instead of start and end. What happens: broken right-to-left layouts. The fix: use start/end everywhere.

Mistake #6: Hard-coded sizes that ignore font scale. What happens: clipped text for users who set a large font. The fix: let text height wrap; use sp for text, and heightIn(min = ...) instead of a fixed height.

Mistake #7: Nesting many Row/Column to fake ConstraintLayout. What happens: unreadable code. The fix: switch to ConstraintLayout when relationships are irregular.

Mistake #8: Using weight outside a Row or Column scope. Symptom: Unresolved reference: weight. The fix: weight exists only inside RowScope and ColumnScope; pass the modifier in from the caller.

Mistake #9: Tiny touch targets. What happens: users struggle to tap. The fix: keep interactive elements at least 48 dp square; Material components do this for you, custom clickable boxes do not.

Mistake #10: Using requiredSize to force a size. What happens: overflow and clipping. The fix: fix the parent's constraints instead.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 11
Knowledge Check

Question 1. How do constraints and sizes flow through the layout tree?

A parent passes constraints (minimum and maximum width and height) down to each child; the child picks a size within them and reports it up; then the parent places the child.

Question 2. Why do Modifier.padding(16.dp).background(c) and Modifier.background(c).padding(16.dp) look different?

Modifiers apply from the outside in. With padding first, the space is outside the background, so the colour covers only the content. With background first, the colour covers the content and the padding.

Question 3. What is the difference between Arrangement and Alignment in a Row?

horizontalArrangement distributes children along the main (horizontal) axis; verticalAlignment positions them on the cross (vertical) axis.

Question 4. When would you choose ConstraintLayout over nested Row and Column?

When children must be positioned relative to each other in irregular ways (chains, barriers, guidelines, many cross dependencies) and nesting would become deep or unclear.

Question 5. Why use Scaffold's innerPadding?

It contains the space taken by the top bar, bottom bar and system bars, so your content is not drawn underneath them.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Recipe Detail Screen

Project goal: Implement a full recipe detail screen from a design you sketch yourself, with a header image area, a title row, an info chip row, an ingredients list, a numbered steps list and a bottom action bar, and make it survive the stress previews.

Requirements:

  1. Scaffold with a TopAppBar (back arrow, title) and a bottom bar containing Start cooking and Save.
  2. A hero area (use a coloured Box if you have no image) with the dish name over it at the bottom start, in a Box.
  3. A row of three info items (time, servings, difficulty), each a small Column, spread with Arrangement.SpaceEvenly.
  4. An ingredients section where each row has the amount on the left (fixed 80 dp) and the name filling the remainder (weight).
  5. A steps section with numbered circles (use Box with CircleShape) beside the step text.
  6. A LazyColumn for the whole screen so it scrolls.
  7. Four previews: normal, small width (320 dp), font scale 1.8 and dark mode. Fix whatever breaks.

Stretch goal: convert the info row to a ConstraintLayout with a guideline at 50%, then decide which version you prefer.

RecipeDetail.kt
package com.example.recipes
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
data class Ingredient(val amount: String, val name: String)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipeDetailScreen(
 title: String,
 ingredients: List<Ingredient>,
 steps: List<String>,
 modifier: Modifier = Modifier
) {
 Scaffold(
 modifier = modifier,
 topBar = { TopAppBar(title = { Text(title) }) },
 bottomBar = {
 BottomAppBar {
 Row(
 Modifier.fillMaxWidth().padding(horizontal = 16.dp),
 horizontalArrangement = Arrangement.spacedBy(12.dp)
 ) {
 Button(onClick = {}, modifier = Modifier.weight(1f)) { Text("Start cooking") }
 OutlinedButton(onClick = {}, modifier = Modifier.weight(1f)) { Text("Save") }
 }
 }
 }
 ) { innerPadding ->
 LazyColumn(
 contentPadding = PaddingValues(
 top = innerPadding.calculateTopPadding(),
 bottom = innerPadding.calculateBottomPadding() + 16.dp
 ),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 item {
 Box(
 Modifier
 .fillMaxWidth()
 .height(200.dp)
 .background(MaterialTheme.colorScheme.secondaryContainer)
 ) {
 Text(
 title,
 style = MaterialTheme.typography.headlineMedium,
 modifier = Modifier.align(Alignment.BottomStart).padding(16.dp)
 )
 }
 }
 item {
 Row(Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.SpaceEvenly) {
 InfoItem("Time", "45 min")
 InfoItem("Serves", "4")
 InfoItem("Level", "Easy")
 }
 }
 item { SectionTitle("Ingredients") }
 items(ingredients.size) { i ->
 Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
 Text(ingredients[i].amount, Modifier.width(80.dp))
 Text(ingredients[i].name, Modifier.weight(1f))
 }
 }
 item { SectionTitle("Steps") }
 itemsIndexed(steps) { index, step ->
 Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.Top) {
 Box(
 Modifier.size(28.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primary),
 contentAlignment = Alignment.Center
 ) { Text("${index + 1}", color = MaterialTheme.colorScheme.onPrimary) }
 Text(step, Modifier.weight(1f).padding(start = 12.dp))
 }
 }
 }
 }
}
@Composable
private fun InfoItem(label: String, value: String) {
 Column(horizontalAlignment = Alignment.CenterHorizontally) {
 Text(value, style = MaterialTheme.typography.titleMedium)
 Text(label, style = MaterialTheme.typography.labelMedium)
 }
}
@Composable
private fun SectionTitle(text: String) {
 Text(text, style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp))
}

Notes: contentPadding takes only the top and bottom of innerPadding so the hero area can span the full width while content still clears the bars. The ingredient amount uses a fixed width(80.dp); at font scale 1.8 an amount like "250 ml" may wrap, which the preview should reveal, and the fix is widthIn(min = 80.dp) so the column can grow. The bottom buttons share the space with weight(1f), so they fit on 320 dp screens. Nothing here is clickable beyond the buttons, so no custom touch-target work is needed.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 38 project”.

3

On github.com click New repository, name it android-lesson-38-layouts-and-modifiers-in-depth, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 38 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: How Layout Works
  • Step-by-Step Build 1: The Big Three
  • Step-by-Step Build 2: Scaffold, Surface, Card and Spacing
  • Step-by-Step Build 3: ConstraintLayout in Compose
  • Step-by-Step Build 4: Constraints and Custom Layout
  • Implement a Real-World Design
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close