Skip to content

Jetpack Compose Layout — Column, Row, Box, and Modifier Guide

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Jetpack Compose Layout. We cover key concepts, practical examples, and best practices to help you master this topic.

Jetpack Compose layout combines Column, Row, Box, and LazyList composables with a flexible modifier system to create responsive UIs that adapt to screen size and content.

What You'll Learn

  • Arrange composables with Column, Row, and Box
  • Use weight, fillMaxWidth, and padding modifiers
  • Create scrollable and lazy lists
  • Build custom layouts with Layout composable
  • Handle responsive layouts for different screen sizes
  • Apply Modifier patterns for common UI patterns
  • Understand intrinsic measurements

Why It Matters

Layout is the foundation of every screen. Compose's approach differs from XML layouts: everything is code, and modifiers chain together to control size, position, and behavior. Understanding how weight distributes space, how lazy lists virtualize content, and how Box layers elements is essential for building any Android UI.

Real-World Use

DodaTech's settings screen uses LazyColumn with sticky headers for categorized preferences. The dashboard uses Row with weight to distribute chart panels evenly. Responsive layouts use BoxWithConstraints to switch between phone and tablet arrangements.

Learning Path

flowchart LR
  A[Compose Basics] --> B[Compose Layout\nYou are here]
  B --> C[ViewModel]
  style B fill:#f90,color:#fff

Column and Row Basics

Column arranges children vertically. Row arranges children horizontally.

@Composable
fun ColumnRowBasics() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // Column fills vertical space
        Text("Column Layout", fontWeight = FontWeight.Bold)
        
        Spacer(modifier = Modifier.height(8.dp))
        
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.LightGray)
                .padding(8.dp),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text("Item 1", modifier = Modifier.weight(1f))
            Text("Item 2", modifier = Modifier.weight(1f))
            Text("Item 3", modifier = Modifier.weight(1f))
        }
        
        Spacer(modifier = Modifier.height(12.dp))
        
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            repeat(4) { index ->
                Box(
                    modifier = Modifier
                        .size(60.dp)
                        .background(Color(index * 60, 100, 200))
                        .padding(4.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text("$index", color = Color.White)
                }
            }
        }
    }
}

Output: A Column with a title, a Row with three equally weighted items, and a Row with evenly spaced colored boxes.

Weight Modifier

The weight modifier distributes remaining space proportionally.

@Composable
fun WeightExample() {
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Row(modifier = Modifier.fillMaxWidth().height(50.dp)) {
            Box(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxHeight()
                    .background(Color.Red),
                contentAlignment = Alignment.Center
            ) {
                Text("1", color = Color.White)
            }
            Box(
                modifier = Modifier
                    .weight(2f)
                    .fillMaxHeight()
                    .background(Color.Green),
                contentAlignment = Alignment.Center
            ) {
                Text("2", color = Color.White)
            }
            Box(
                modifier = Modifier
                    .weight(3f)
                    .fillMaxHeight()
                    .background(Color.Blue),
                contentAlignment = Alignment.Center
            ) {
                Text("3", color = Color.White)
            }
        }
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // Nested weights
        Row(modifier = Modifier.fillMaxWidth()) {
            Column(modifier = Modifier.weight(1f)) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(40.dp)
                        .background(Color.Magenta)
                )
            }
            Spacer(modifier = Modifier.width(8.dp))
            Column(modifier = Modifier.weight(2f)) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(40.dp)
                        .background(Color.Cyan)
                )
                Spacer(modifier = Modifier.height(8.dp))
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(40.dp)
                        .background(Color.Yellow)
                )
            }
        }
    }
}

Output: The first Row shows a 1:2:3 ratio. The second section shows nested weighted layouts.

Box for Layering

Box layers children on top of each other with alignment control.

@Composable
fun BoxExample() {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp)
            .padding(16.dp)
    ) {
        // Background layer
        Box(
            modifier = Modifier
                .matchParentSize()
                .background(
                    Brush.verticalGradient(
                        colors = listOf(Color(0xFF2196F3), Color(0xFF1976D2))
                    )
                )
                .clip(RoundedCornerShape(16.dp))
        )
        
        // Content layer
        Column(
            modifier = Modifier
                .align(Alignment.Center)
                .padding(24.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                "Title Overlay",
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold,
                color = Color.White
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                "This text is centered on the gradient background",
                color = Color.White.copy(alpha = 0.8f)
            )
        }
        
        // Top-right badge
        Box(
            modifier = Modifier
                .align(Alignment.TopEnd)
                .padding(8.dp)
                .background(Color.Red, CircleShape)
                .size(32.dp),
            contentAlignment = Alignment.Center
        ) {
            Text("3", color = Color.White, fontSize = 14.sp)
        }
    }
}

Output: A gradient card with centered text and a badge in the top-right corner. Box layers the badge on top of the content.

LazyColumn and LazyRow

Lazy lists only compose visible items, making them efficient for large datasets.

data class Contact(val name: String, val phone: String, val avatar: Color)

@Composable
fun ContactList(contacts: List<Contact>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Sticky header
        stickyHeader {
            Text(
                "Contacts",
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold,
                modifier = Modifier
                    .fillMaxWidth()
                    .background(MaterialTheme.colorScheme.surface)
                    .padding(vertical = 8.dp)
            )
        }
        
        // Items
        items(contacts) { contact ->
            ContactItem(contact)
        }
        
        // Section header in between
        item {
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                "Favorites",
                fontSize = 18.sp,
                fontWeight = FontWeight.Medium
            )
        }
        
        // Multiple items from a list
        items(contacts.take(3)) { contact ->
            FavoriteItem(contact)
        }
    }
}

@Composable
fun ContactItem(contact: Contact) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.White, RoundedCornerShape(12.dp))
            .padding(12.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box(
            modifier = Modifier
                .size(48.dp)
                .background(contact.avatar, CircleShape),
            contentAlignment = Alignment.Center
        ) {
            Text(
                contact.name.first().toString(),
                color = Color.White,
                fontWeight = FontWeight.Bold
            )
        }
        
        Spacer(modifier = Modifier.width(12.dp))
        
        Column {
            Text(contact.name, fontWeight = FontWeight.Medium)
            Text(
                contact.phone,
                fontSize = 14.sp,
                color = Color.Gray
            )
        }
    }
}

@Composable
fun FavoriteItem(contact: Contact) {
    // Similar to ContactItem with favorite styling
}

Output: A scrollable list of contacts with sticky header, section dividers, and efficient recycling of off-screen items.

LazyRow for Horizontal Scrolling

@Composable
fun CategoryChips(categories: List<String>) {
    LazyRow(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.dp),
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(categories) { category ->
            SuggestionChip(
                onClick = { /* filter by category */ },
                label = { Text(category) },
                modifier = Modifier.wrapContentWidth()
            )
        }
    }
}

@Composable
fun ScrollableContent() {
    Column {
        Text(
            "Horizontal Categories",
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
        )
        
        val categories = listOf("All", "Work", "Personal", "Shopping", "Travel", "Finance", "Health")
        CategoryChips(categories)
        
        Text(
            "Items",
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
        )
        
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(horizontal = 16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(50) { index ->
                Text(
                    "Item #$index",
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.LightGray, RoundedCornerShape(8.dp))
                        .padding(16.dp)
                )
            }
        }
    }
}

Output: Horizontal scrolling chips with a vertical lazy list below. The chips scroll independently of the main list.

BoxWithConstraints for Responsive Layout

BoxWithConstraints provides access to the available space for responsive designs.

@Composable
fun ResponsiveLayout() {
    BoxWithConstraints(
        modifier = Modifier.fillMaxSize()
    ) {
        val isWide = maxWidth > 600.dp
        
        if (isWide) {
            // Tablet layout: side by side
            Row(modifier = Modifier.fillMaxSize()) {
                Panel(
                    modifier = Modifier
                        .weight(1f)
                        .fillMaxHeight()
                )
                VerticalDivider()
                Panel(
                    modifier = Modifier
                        .weight(1f)
                        .fillMaxHeight()
                )
            }
        } else {
            // Phone layout: stacked
            Column(modifier = Modifier.fillMaxSize()) {
                Panel(modifier = Modifier.weight(1f))
                HorizontalDivider()
                Panel(modifier = Modifier.weight(1f))
            }
        }
    }
}

@Composable
fun Panel(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
            .padding(8.dp)
            .background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp)),
        contentAlignment = Alignment.Center
    ) {
        Text("Panel", fontSize = 18.sp)
    }
}

@Composable
fun ProfileScreen(profileName: String) {
    BoxWithConstraints(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        val availableWidth = maxWidth
        
        if (availableWidth > 400.dp) {
            // Wide: avatar and text side by side
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.fillMaxWidth()
            ) {
                Avatar(size = 64.dp)
                Spacer(modifier = Modifier.width(16.dp))
                Column {
                    Text(profileName, fontWeight = FontWeight.Bold)
                    Text("Online")
                }
            }
        } else {
            // Narrow: stacked
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.fillMaxWidth()
            ) {
                Avatar(size = 48.dp)
                Text(profileName, fontWeight = FontWeight.Bold)
                Text("Online", fontSize = 14.sp, color = Color.Gray)
            }
        }
    }
}

Output: On wider screens, the layout switches from stacked to side-by-side automatically.

Custom Layout with Layout Composable

For complex arrangements, use the Layout composable with custom measurement and placement.

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    columns: Int = 2,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        // Measure each child
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints.copy(
                maxWidth = constraints.maxWidth / columns
            ))
        }
        
        // Layout
        val columnHeights = IntArray(columns) { 0 }
        val positions = mutableListOf<Pair<Int, Int>>()
        
        for (placeable in placeables) {
            val shortestColumn = columnHeights.indices.minByOrNull { columnHeights[it] } ?: 0
            val x = shortestColumn * (constraints.maxWidth / columns)
            val y = columnHeights[shortestColumn]
            positions.add(x to y)
            columnHeights[shortestColumn] += placeable.height
        }
        
        val totalHeight = columnHeights.maxOrNull() ?: 0
        
        layout(constraints.maxWidth, totalHeight) {
            placeables.forEachIndexed { index, placeable ->
                val (x, y) = positions[index]
                placeable.placeRelative(x, y)
            }
        }
    }
}

@Composable
fun StaggeredGridExample() {
    StaggeredGrid(
        columns = 2,
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
    ) {
        val heights = listOf(80.dp, 120.dp, 60.dp, 100.dp, 140.dp, 90.dp)
        val colors = listOf(Color.Red, Color.Green, Color.Blue, Color.Magenta, Color.Cyan, Color.Yellow)
        
        heights.forEachIndexed { index, height ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(height)
                    .padding(4.dp)
                    .background(colors[index], RoundedCornerShape(8.dp)),
                contentAlignment = Alignment.Center
            ) {
                Text("Item $index", color = Color.White)
            }
        }
    }
}

Output: Items are placed in a staggered grid pattern, each filling the shortest column, similar to Pinterest-style layouts.

Common Mistakes

  1. Using Column inside LazyColumn for each item: LazyColumn items should be individual composables. Using Column inside .items adds unnecessary nesting. Create a dedicated item composable.

  2. Not specifying content scale for images: Images without contentScale crop or stretch unexpectedly. Use contentScale = ContentScale.Crop or ContentScale.Fit.

  3. Overusing padding on individual children instead of contentPadding: LazyColumn's contentPadding is more efficient than padding every item separately.

  4. Forgetting Arrangement.spacedBy: Instead of adding Spacer between every item, use Arrangement.spacedBy on Column or LazyColumn.

  5. Ignoring intrinsic measurements: Compose measures twice (intrinsics and actual). Understanding intrinsicSize helps with performance.

  6. Modifier order issues: Remember that modifiers are applied from outer to inner. background before padding colors the padding area as well.

Practice Questions

  1. How does the weight modifier distribute space in a Row?

Answer: Weight distributes remaining space proportionally. If three children have weights 1, 2, 3, they get 1/6, 2/6, and 3/6 of the remaining space after fixed-size children are placed.

  1. What is the difference between LazyColumn and Column with verticalScroll?

Answer: LazyColumn only composes visible items, recycling off-screen ones. Column with verticalScroll composes all items regardless of visibility, which is inefficient for large lists.

  1. How does BoxWithConstraints enable responsive layouts?

Answer: BoxWithConstraints provides maxWidth and maxHeight from the parent's constraints. You can use these values to choose different layouts for different screen sizes.

  1. What does matchParentSize do in a Box?

Answer: matchParentSize makes a child match the Box's size without affecting the Box's own sizing. It is useful for background layers.

  1. Challenge: Create a movie poster grid that switches from 2 columns on phones to 4 columns on tablets. Use LazyVerticalGrid with adaptive sizing and BoxWithConstraints for the column count.

Answer:

@Composable
fun MovieGrid(movies: List<String>) {
    BoxWithConstraints {
        val columns = if (maxWidth > 720.dp) 4 else 2
        
        LazyVerticalGrid(
            columns = GridCells.Fixed(columns),
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(movies) { movie ->
                MovieCard(movie)
            }
        }
    }
}

@Composable
fun MovieCard(title: String) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp)
            .background(Color.DarkGray, RoundedCornerShape(8.dp))
            .clickable { /* open movie details */ },
        contentAlignment = Alignment.Center
    ) {
        Text(title, color = Color.White, fontWeight = FontWeight.Bold)
    }
}

Mini Project

Build a news reader app layout. Requirements:

  • Top app bar with title and search icon
  • LazyRow for category chips (horizontal scrolling)
  • LazyColumn for news article cards
  • Each card has an image placeholder, title, description, and timestamp
  • Pull-to-refresh at the top
  • Responsive: show 1 card per row on phones, 2 on tablets
  • Sticky header for "Today's Headlines" section

This project applies all layout concepts in a realistic, content-rich application.

FAQ

What is the difference between IntrinsicSize.Min and IntrinsicSize.Max?

IntrinsicSize.Min is the minimum size a composable can be while still rendering correctly. IntrinsicSize.Max is the size it would like to be given unlimited space. Use intrinsicSize for wrapping content in constrained layouts.

How do I create a vertical divider in Compose?

Use VerticalDivider() from Material 3 or a Box with width = 1.dp, fillMaxHeight(), and a background color.

Can I use ConstraintLayout in Compose?

Yes. Add the constraintlayout-compose dependency. It works similarly to the XML ConstraintLayout but with a Kotlin DSL.

What is the difference between fillMaxWidth and matchParentSize?

fillMaxWidth sets the composable's width to the maximum available. matchParentSize is used inside a Box to make the child match the Box's measured size without affecting the Box's measurement.

How do I create rounded corners on a composable?

Use Modifier.clip(RoundedCornerShape(16.dp)) or Modifier.clip(CircleShape) for circular clipping.

What's Next

After mastering layouts, learn ViewModel for state management. You can also explore Room database for local data persistence.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro