Layouts: Column, Row & Box
Compose has no XML LinearLayout or ConstraintLayout — instead, three
small composable functions cover almost every arrangement you'll need: Column
stacks its children vertically, Row stacks them horizontally, and
Box layers them on top of one another. Every screen in RecipeBox, no matter how
complex, is built by nesting these three.
Column: Vertical Stacking
Column places each child directly below the previous one. Use
verticalArrangement to control the spacing between children, and
horizontalAlignment to control how children line up on the cross axis:
@Composable
fun LoginScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "RecipeBox", style = MaterialTheme.typography.headlineSmall)
OutlinedTextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
OutlinedTextField(value = password, onValueChange = { password = it }, label = { Text("Password") })
Button(onClick = onLoginClick) { Text("Log In") }
}
}
Arrangement.spacedBy(12.dp) puts a fixed gap between every pair of children — far
less error-prone than adding a Spacer or bottom padding to each one individually.
Other common values are Arrangement.Center, Arrangement.SpaceBetween,
and Arrangement.SpaceEvenly.
Row: Horizontal Stacking
Row is Column's horizontal twin — children flow left to right, and the equivalent
parameters are horizontalArrangement and verticalAlignment:
@Composable
fun RecipeMetaRow(prepMinutes: Int, category: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "$prepMinutes min", style = MaterialTheme.typography.bodySmall)
Text(text = category, style = MaterialTheme.typography.labelMedium)
}
}
Arrangement.SpaceBetween pushes the first child to the start and the last child to
the end, distributing any remaining space evenly between them — useful here to push the prep time
to the left and the category label to the right of the same row.
Box: Overlapping Content
Box is the odd one out — instead of stacking children in sequence, it layers every
child on top of the others at the same position, by default aligned to the top-left. You control
each child's position individually with Modifier.align(...), scoped to that
Box:
@Composable
fun RecipePhotoWithFavorite(photoUri: String, isFavorite: Boolean) {
Box(modifier = Modifier.size(96.dp)) {
AsyncImage(
model = photoUri,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.matchParentSize()
)
Icon(
imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = "Favorite",
tint = Color.Red,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(4.dp)
)
}
}
Here the recipe photo fills the whole Box via matchParentSize(), and the
favorite-heart icon floats on top of it, pinned to the top-right corner with
Alignment.TopEnd — exactly the overlapping badge effect you see in most recipe and
shopping apps.
Worked Example: A Full RecipeBox List Item
A real recipe row combines all three: a Box for the photo-plus-favorite-icon (built
above), sitting inside a Row alongside a Column that stacks the title
over the metadata:
@Composable
fun RecipeListItem(
title: String,
category: String,
prepMinutes: Int,
photoUri: String,
isFavorite: Boolean,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Box: photo with an overlapping favorite badge
Box(modifier = Modifier.size(72.dp)) {
AsyncImage(
model = photoUri,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.matchParentSize()
)
if (isFavorite) {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = "Favorite",
tint = Color.Red,
modifier = Modifier.align(Alignment.TopEnd).padding(2.dp)
)
}
}
// Column: title stacked above the metadata row
Column(modifier = Modifier.weight(1f)) {
Text(text = title, style = MaterialTheme.typography.titleMedium, maxLines = 1)
// Row: prep time and category side by side
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "$prepMinutes min", style = MaterialTheme.typography.bodySmall)
Text(text = category, style = MaterialTheme.typography.bodySmall)
}
}
}
}
Modifier.weight(1f) inside the outer Row tells the Column to
take up all the remaining horizontal space after the fixed-size photo Box — so the
title and metadata stretch to fill the row regardless of screen width, while the photo stays a
constant 72.dp square.
Text throughout this lesson without looking
closely at it. Text & Typography covers its key
parameters and Material 3's typography presets in depth.