Jetpack Compose: Staggered Grid Layout

Jetpack Compose, Android’s modern UI toolkit, provides a powerful and flexible way to build user interfaces. While it offers built-in layouts like Column and Row, creating complex layouts like a staggered grid requires a bit more effort. A staggered grid, also known as a masonry layout, is a grid where the sizes of the items may vary, creating an appealing, dynamic look.

What is a Staggered Grid Layout?

A staggered grid layout arranges items in a grid-like structure, but the sizes (especially the heights) of the items are not uniform. This variation in size creates a dynamic and visually appealing effect. This type of layout is often seen in Pinterest and other image-heavy applications.

Why Use a Staggered Grid?

  • Visual Appeal: Provides a dynamic and interesting layout.
  • Content Emphasis: Allows certain items to stand out based on size.
  • Efficient Use of Space: Adapts well to varying content sizes, maximizing screen real estate.

How to Implement a Staggered Grid Layout in Jetpack Compose

Currently, Jetpack Compose does not offer a built-in staggered grid layout. However, we can implement one using custom layouts or libraries.

Method 1: Using LazyVerticalStaggeredGrid (Requires androidx.compose.foundation:foundation-layout 1.6.0 or later)

The LazyVerticalStaggeredGrid API provides a way to build staggered grids with varying spans using the StaggeredGridCells and LazyStaggeredGridItemScope.StaggeredGridItemSpan APIs.

Step 1: Add Dependency

Ensure you have the latest Compose Foundation dependency in your build.gradle file:

dependencies {
    implementation("androidx.compose.foundation:foundation-layout:1.6.0") // or newer
}
Step 2: Implement LazyVerticalStaggeredGrid

Here’s an example of how to create a staggered grid using LazyVerticalStaggeredGrid:


import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlin.random.Random

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun StaggeredGridLayout() {
    val items = (1..20).map { "Item $it" }

    LazyVerticalStaggeredGrid(
        columns = StaggeredGridCells.Adaptive(minSize = 120.dp),
        modifier = Modifier.fillMaxWidth(),
        contentPadding = PaddingValues(16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(items.size) { index ->
            val randomHeight = Random.nextInt(50, 150).dp // Different heights
            val item = items[index]
            Card(
                modifier = Modifier.fillMaxWidth()
            ) {
                Text(
                    text = item,
                    modifier = Modifier.padding(8.dp)
                )
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun StaggeredGridLayoutPreview() {
    StaggeredGridLayout()
}

In this example:

  • LazyVerticalStaggeredGrid creates a vertically scrolling staggered grid.
  • StaggeredGridCells.Adaptive(minSize = 120.dp) sets up the columns to adapt to the screen size with a minimum width of 120dp.
  • Each item has a random height between 50 and 150 dp, creating the staggered effect.

Method 2: Using a Custom Layout

For more control and customization, you can create a custom staggered grid layout using Compose’s Layout composable.

Step 1: Create a Custom Layout Composable

Implement a custom layout composable that manually positions the children in a staggered manner.


import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
import androidx.compose.foundation.layout.*
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.Constraints
import kotlin.random.Random

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    columnCount: Int = 2,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp),
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        val columnWidth = (constraints.maxWidth / columnCount)
        val itemConstraints = Constraints.fixedWidth(columnWidth)

        val placeables = measurables.map { measurable ->
            measurable.measure(itemConstraints)
        }

        val columnHeights = IntArray(columnCount) { 0 } // Heights of each column

        layout(
            width = constraints.maxWidth,
            height = placeables.sumOf { it.height } // Simplified height calculation, can be improved
        ) {
            placeables.forEachIndexed { index, placeable ->
                val shortestColumnIndex = columnHeights.indexOf(columnHeights.minOrNull() ?: 0)
                val x = shortestColumnIndex * columnWidth
                val y = columnHeights[shortestColumnIndex]

                placeable.placeRelative(x = x, y = y)
                columnHeights[shortestColumnIndex] += placeable.height
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun StaggeredGridPreview() {
    val items = (1..10).map { "Item $it" }

    StaggeredGrid(columnCount = 2, modifier = Modifier.padding(16.dp)) {
        items.forEach { item ->
            val randomHeight = Random.nextInt(50, 150).dp
            Card(modifier = Modifier.height(randomHeight)) {
                Box(contentAlignment = Alignment.Center) {
                    Text(text = item)
                }
            }
        }
    }
}

Key components in this custom layout:

  • Column Width Calculation: Determines the width of each column based on the provided columnCount.
  • Item Measurement: Measures each item with fixed-width constraints to fit within the columns.
  • Column Height Tracking: An array columnHeights tracks the height of each column to find the shortest column for placing the next item.
  • Placement Logic: Iterates through each item, places it in the shortest column, and updates the column’s height.
Step 2: Usage

Use the StaggeredGrid composable with your content:


import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlin.random.Random

@Composable
fun StaggeredGridItem(text: String) {
    val randomHeight = Random.nextInt(50, 150).dp
    Card(modifier = Modifier.height(randomHeight).padding(4.dp)) {
        Text(text = text, modifier = Modifier.padding(8.dp))
    }
}

@Composable
fun StaggeredGridExample() {
    val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6")
    
    StaggeredGrid(columnCount = 2, modifier = Modifier.padding(16.dp)) {
        items.forEach { item ->
            StaggeredGridItem(text = item)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun StaggeredGridExamplePreview() {
    StaggeredGridExample()
}

Conclusion

Creating a staggered grid layout in Jetpack Compose can be achieved through LazyVerticalStaggeredGrid or a custom layout approach. While LazyVerticalStaggeredGrid offers simplicity for basic implementations, a custom layout provides greater control and flexibility for complex designs. Choose the method that best fits your project’s requirements and level of customization needed. Properly implementing a staggered grid can greatly enhance the visual appeal and user experience of your Android applications.