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.

Next up: We used Text throughout this lesson without looking closely at it. Text & Typography covers its key parameters and Material 3's typography presets in depth.