Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 017 / 100

Compose layouts: Column, Row and Box in Android & Kotlin

Building blocks for any screen.

Expona Admin8 min read

Compose layouts: Column, Row and Box in Android & Kotlin

When you're building Android screens with Jetpack Compose, you're essentially arranging UI elements into a visual hierarchy. The three foundational layout composables—Column, Row, and Box—form the building blocks for virtually every screen you'll create. Understanding how they work and when to use each one is critical to writing maintainable, performant Compose code.

Why layouts matter in Compose

Unlike the old View system where you'd nest LinearLayout or FrameLayout hierarchies, Compose gives you composable functions that handle layout logic directly. These composables are lightweight, reusable, and compose-aware—meaning they respond naturally to state changes and recomposition. When you understand Column, Row, and Box, you stop fighting the framework and start leveraging its core strengths.

Layout composables in Compose don't just position children; they also manage constraint solving, spacing, and alignment in a declarative way. This means less boilerplate, fewer layout passes, and fewer rendering bugs compared to imperative View layouts.

Column: stacking vertically

A Column is a layout that arranges its children in a vertical stack, with each child placed below the previous one. It's the Compose equivalent of a vertical LinearLayout. When you need to display a list of items, form fields, or any vertically-oriented content, Column is your go-to.

Here's a basic example of a settings screen using Column:

kotlin
@Composable
fun SettingsScreen() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .verticalScroll(rememberScrollState())
    ) {
        Text(
            text = "Settings",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier.padding(bottom = 24.dp)
        )
        
        SettingsSwitch(
            title = "Enable Notifications",
            isChecked = true
        )
        
        SettingsSwitch(
            title = "Dark Mode",
            isChecked = false
        )
        
        Button(
            onClick = { /* Handle logout */ },
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 32.dp)
        ) {
            Text("Logout")
        }
    }
}

@Composable
fun SettingsSwitch(title: String, isChecked: Boolean) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 12.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(title)
        Switch(checked = isChecked, onCheckedChange = {})
    }
}

In this example, Column stacks the title, switches, and logout button vertically. We also added verticalScroll to make the entire Column scrollable if content exceeds the screen height. Notice how we use modifier to control width, padding, and scroll behavior—this is the Compose way.

Column also accepts arrangement parameters to control spacing between children. The default is Arrangement.Top, but you can use Arrangement.Center, Arrangement.Bottom, Arrangement.SpaceEvenly, or Arrangement.SpaceBetween depending on your design needs.

Row: arranging horizontally

A Row is the horizontal counterpart to Column. It lays out children from left to right (or right to left in RTL layouts). Rows are essential for building button groups, navigation bars, profile cards with avatars and text, and any side-by-side layouts.

Consider a product card in an e-commerce app:

kotlin
@Composable
fun ProductCard(product: Product) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(12.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(Color.White)
            .padding(12.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Image(
            painter = painterResource(id = product.imageRes),
            contentDescription = product.name,
            modifier = Modifier
                .size(80.dp)
                .clip(RoundedCornerShape(4.dp)),
            contentScale = ContentScale.Crop
        )
        
        Column(
            modifier = Modifier
                .weight(1f)
                .align(Alignment.CenterVertically)
        ) {
            Text(
                text = product.name,
                style = MaterialTheme.typography.titleMedium,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
            Text(
                text = product.description,
                style = MaterialTheme.typography.bodySmall,
                color = Color.Gray,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.padding(vertical = 4.dp)
            )
            Text(
                text = "$${product.price}",
                style = MaterialTheme.typography.titleSmall,
                color = Color(0xFF2E7D32)
            )
        }
        
        IconButton(onClick = { /* Add to cart */ }) {
            Icon(Icons.Default.ShoppingCart, contentDescription = "Add to cart")
        }
    }
}

data class Product(
    val name: String,
    val description: String,
    val price: Double,
    val imageRes: Int
)

Notice the weight(1f) on the Column—this tells Compose to give the text column all remaining horizontal space after the image and icon button take their space. This is a powerful pattern for responsive layouts. The horizontalArrangement = Arrangement.spacedBy(12.dp) automatically adds 12dp spacing between children without manual padding.

Rows also respect vertical alignment through verticalAlignment. In the example above, we centered the text column vertically relative to the image using Alignment.CenterVertically.

Box: layering children

A Box is a layout that places all its children on top of each other, with the first child at the back and the last child at the front. Boxes are perfect for layering content—think badges on icons, gradients behind text, or overlays on images.

Here's a practical example of a product image with a sale badge:

kotlin
@Composable
fun ProductImageWithBadge(imageRes: Int, discountPercent: Int) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(Color.LightGray)
    ) {
        Image(
            painter = painterResource(id = imageRes),
            contentDescription = "Product image",
            modifier = Modifier.fillMaxSize(),
            contentScale = ContentScale.Crop
        )
        
        // Sale badge in top-right corner
        if (discountPercent > 0) {
            Box(
                modifier = Modifier
                    .align(Alignment.TopEnd)
                    .padding(8.dp)
                    .size(40.dp)
                    .clip(CircleShape)
                    .background(Color.Red),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = "-$discountPercent%",
                    color = Color.White,
                    style = MaterialTheme.typography.labelSmall,
                    textAlign = TextAlign.Center
                )
            }
        }
        
        // Dark overlay gradient at bottom
        Box(
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .fillMaxWidth()
                .height(60.dp)
                .background(
                    brush = Brush.verticalGradient(
                        colors = listOf(
                            Color.Transparent,
                            Color.Black.copy(alpha = 0.6f)
                        )
                    )
                )
        )
    }
}

In this example, we layer three elements: the product image as the base, a sale badge aligned to the top-right, and a gradient overlay at the bottom. Box handles all of this with align and contentAlignment to position each layer precisely. This is much cleaner than trying to achieve similar effects with nested FrameLayouts.

Box also accepts a contentAlignment parameter that centers all children by default alignment, which is useful when you want a single centered item.

Arrangement and Alignment: controlling spacing and positioning

Layouts become powerful when you combine them with Arrangement and Alignment. Arrangement controls how children are distributed along the main axis (horizontally in Row, vertically in Column). Alignment controls positioning along the cross-axis.

Common arrangement options:

  • Arrangement.Start – children packed at the beginning
  • Arrangement.End – children packed at the end
  • Arrangement.Center – children centered
  • Arrangement.SpaceBetween – first and last at edges, others evenly spaced
  • Arrangement.SpaceEvenly – equal space between and around all children
  • Arrangement.SpaceAround – equal space around children (half-space at edges)
  • Arrangement.spacedBy(gap) – fixed gap between consecutive children

Here's a practical navigation bar example that demonstrates these concepts:

kotlin
@Composable
fun BottomNavigationBar(selectedTab: Int, onTabSelected: (Int) -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(56.dp)
            .background(MaterialTheme.colorScheme.primary),
        horizontalArrangement = Arrangement.SpaceEvenly,
        verticalAlignment = Alignment.CenterVertically
    ) {
        repeat(4) { index ->
            NavigationItem(
                icon = Icons.Default.Home, // would vary per tab
                label = "Tab $index",
                isSelected = selectedTab == index,
                onClick = { onTabSelected(index) }
            )
        }
    }
}

@Composable
fun NavigationItem(
    icon: androidx.compose.material.icons.Icons,
    label: String,
    isSelected: Boolean,
    onClick: () -> Unit
) {
    Column(
        modifier = Modifier
            .clickable(onClick = onClick)
            .padding(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Icon(
            imageVector = icon,
            contentDescription = label,
            tint = if (isSelected) Color.White else Color.White.copy(alpha = 0.6f)
        )
        Text(
            text = label,
            color = if (isSelected) Color.White else Color.White.copy(alpha = 0.6f),
            style = MaterialTheme.typography.labelSmall,
            modifier = Modifier.padding(top = 4.dp)
        )
    }
}

The Row uses Arrangement.SpaceEvenly to give each navigation item equal horizontal space, and Alignment.CenterVertically to vertically center them. The nested Column centers its icon and text horizontally.

Common mistakes

Forgetting to set width on Row children: If you don't specify width or use fillMaxWidth(), Row children can overflow. Always think about sizing constraints.

Nesting too many Boxes: While Box is useful for layering, excessive nesting can hurt performance and readability. Use it purposefully, not as a catch-all wrapper.

Not using weight() for responsive layouts: When you want a child to expand and fill available space, weight(1f) is your friend. Without it, your layout may look broken on different screen sizes.

Ignoring scrolling needs: If your Column or Row content might exceed screen bounds, add .verticalScroll(rememberScrollState()) or .horizontalScroll(rememberScrollState()). Users expect smooth scrolling, and your app should handle overflow gracefully.

Mixing incompatible alignment strategies: Don't combine contentAlignment on Box with conflicting align() calls on children—the behavior becomes unpredictable. Pick one strategy and stick with it.

Overusing Box when Column or Row suffice: If you're just stacking items vertically or horizontally, use Column or Row. Save Box for true layering scenarios like badges or overlays.

Key takeaways

  • Column stacks children vertically and is the foundation for most vertical flows like settings screens and form layouts.
  • Row arranges children horizontally and is essential for side-by-side layouts like navigation bars, cards with avatars, and button groups.
  • Box layers children on top of each other and excels at creating badges, overlays, and depth effects.
  • Arrangement controls spacing along the main axis; use spacedBy() for consistent gaps or SpaceEvenly for even distribution.
  • Alignment controls positioning along the cross-axis; combine it with align() on individual children for precise placement.
  • Always specify size constraints—use fillMaxWidth(), wrapContentSize(), or explicit dimensions to avoid layout surprises.
  • Use weight(1f) to make children expand and fill available space, enabling responsive layouts that adapt to different screen sizes.
  • Remember to add scrolling modifiers when content might overflow, and test your layouts on multiple device sizes.
  • Keep your layout hierarchy shallow and readable; deeply nested layouts are harder to maintain and can impact performance.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles