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,ColumnandBoxwithArrangementandAlignment. - Share space with
weight,fillMaxWidthand fractions. - Predict the effect of modifier order.
- Explain how constraints flow down and sizes flow up.
- Use
Scaffold,Surface,CardandSpacerproperly. - Use
ConstraintLayoutin Compose when nesting gets awkward. - Use
Modifier.layoutand 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.
- The three phases and Compose basics (lesson 37). Layout is phase two.
Row,Column,Modifierfirst steps (lesson 16).- dp and sp units (lesson 16).
dpfor sizes and spacing,spfor text. - Preview (lesson 15). Use previews with
widthDp,fontScaleandlocaleoptions throughout.
Constraints down, sizes up
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 withrequiredSize, 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.
// 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))
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:
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.
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 |
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):
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:
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.
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 = ...).
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:
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 aColumnorRowis cleaner than putting a Spacer between every pair.Modifier.paddingfor space around a composable;contentPaddingon 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.
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):
[versions]
constraintlayoutCompose = "<latest stable>"
[libraries]
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayoutCompose" }
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.
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 bothtopandbottomto the parent centres the item vertically.margin = 12.dpis spacing from the target.- You can also use
createGuidelineFromStart(0.3f),createBarrier(...)andcreateHorizontalChain(...), which have no easy Row/Column equivalent, andDimension.fillToConstraintsto 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.
Seeing constraints at work
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:
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:
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.
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:
+------------------------------------------+
| [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:
- Outer shape: a
Cardcontaining aColumn. - Row 1: a
Boxor image 180 dp tall,fillMaxWidth,ContentScale.Crop. - Row 2: a
Rowwith the title takingweight(1f)and anIconButtonat the end. - Row 3: a
Rowof threeTexts separated by dots, or a single formattedText. - Body: a
Textwith a max of 3 lines and an ellipsis. - Actions: a
RowwithArrangement.SpaceBetween. - Spacing: use 16 dp content padding and 8 dp between rows, via
paddingandspacedBy.
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:
@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).
Exercise 1: Predict the order
For each snippet, draw what you expect (size of the red area, where taps register) before running it.
// 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))
- The red area is 80 dp square (120 dp minus 20 dp padding on each side), because
sizecomes first and padding eats from it. - The red area is 120 dp square; 20 dp of empty margin surrounds it, so the whole element occupies 160 dp.
backgroundis first, so the red covers everything inside it: padding (20 dp each side) plus the 80 dp content: 120 dp red square in total.- 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.
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.
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.
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?
@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.
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.
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.
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
- Jetpack Compose for Android developers, Pathway 1: Compose essentials
- Pathway 2: Layouts, theming and animation
- Jetpack Compose layouts codelab
- Jetpack Compose course page
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:
Scaffoldwith aTopAppBar(back arrow, title) and a bottom bar containing Start cooking and Save.- A hero area (use a coloured
Boxif you have no image) with the dish name over it at the bottom start, in aBox. - A row of three info items (time, servings, difficulty), each a small
Column, spread withArrangement.SpaceEvenly. - An ingredients section where each row has the amount on the left (fixed 80 dp) and the name filling the remainder (
weight). - A steps section with numbered circles (use
BoxwithCircleShape) beside the step text. - A
LazyColumnfor the whole screen so it scrolls. - 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.
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.
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.
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”.
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.
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.
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