Jetpack Compose: Building Complex Grids

Jetpack Compose, the modern UI toolkit for Android, provides a declarative approach to building user interfaces. While Compose offers basic layout components like Column, Row, and Box, creating complex grids often requires more advanced techniques. Understanding how to effectively combine these basic components and leverage Compose’s flexibility is crucial for developing sophisticated UIs.

What Constitutes a Complex Grid in Jetpack Compose?

A complex grid in Jetpack Compose extends beyond simple row-and-column arrangements. It might include:

  • Variable row and column spans for individual grid items.
  • Items of differing sizes and aspect ratios.
  • Dynamic grids that adjust to different screen sizes and orientations.
  • Nested grids within grids for hierarchical layouts.

Why Build Complex Grids in Jetpack Compose?

  • Enhanced UI/UX: Offers better presentation and organization of content, leading to improved user experience.
  • Responsive Design: Adapts seamlessly to various screen sizes and orientations.
  • Content Emphasis: Allows specific items to stand out based on their placement and size within the grid.

Methods for Building Complex Grids in Jetpack Compose

There are several approaches to building complex grids, depending on your specific requirements.

Method 1: Using LazyVerticalGrid and Spans

The LazyVerticalGrid composable, combined with column and row spans, is a fundamental way to create complex grid layouts. This allows elements to occupy multiple rows or columns.

Step 1: Add Dependency

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

dependencies {
    implementation "androidx.compose.foundation:foundation-layout:1.6.0" //or newer
    implementation "androidx.compose.material3:material3:1.3.0"
    implementation "androidx.compose.ui:ui:1.6.0"
}
Step 2: Implement the LazyVerticalGrid

Here’s how to create a complex grid layout using LazyVerticalGrid:


import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.Card
import androidx.compose.material3.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 androidx.compose.ui.Alignment
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.graphics.Color
import kotlin.random.Random

data class GridItem(val id: Int, val text: String, val columnSpan: Int = 1, val rowSpan: Int = 1, val height: Dp = 100.dp)

@Composable
fun ComplexGrid(items: List) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(4), // Define 4 columns
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        modifier = Modifier.fillMaxSize()
    ) {
        items(items = items,
            key = { it.id },
            itemContent = { item ->
            Card(
                modifier = Modifier
                    .padding(4.dp)
                    .aspectRatio(1f) // Adjust aspect ratio if needed
                    .then(Modifier.height(item.height))
                ,
            ) {
                androidx.compose.foundation.layout.Box(modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center)
                 {
                        Text(text = item.text)
                }
            }
        })
    }
}

@Preview(showBackground = true)
@Composable
fun ComplexGridPreview() {
     val items = listOf(
        GridItem(1, "Item 1", columnSpan = 2),
        GridItem(2, "Item 2"),
        GridItem(3, "Item 3"),
        GridItem(4, "Item 4"),
        GridItem(5, "Item 5"),
        GridItem(6, "Item 6"),
        GridItem(7, "Item 7", columnSpan = 4, height = 200.dp),
        GridItem(8, "Item 8"),
        GridItem(9, "Item 9"),
        GridItem(10, "Item 10")
    )

    ComplexGrid(items = items)
}

Explanation:

  • LazyVerticalGrid organizes items in a scrollable vertical grid.
  • GridCells.Fixed(4) defines the grid with 4 columns.
  • itemContent dictates what should render for each individual item in the `LazyVerticalGrid`
  • The items property is populated via a list of `GridItem` objects which hold their properties (height, text, span, ID)
  • Modifier.aspectRatio(1f) is set to make the Card item occupy space inside the available container via a ratio(eg: 1:1 ratio)
  • Modifier.then is being chained with the Modifier height so it doesn’t break when Modifier.aspectRatio() applies on it’s own

Method 2: Using Nested Layouts

Nested layouts involve combining Column, Row, and Box to create more intricate grid structures. This offers great flexibility but can become complex.

Step 1: Define the Layout Structure

Use nested Column and Row composables to create a grid-like arrangement:


import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.random.Random

@Composable
fun NestedGridLayout() {
    Column(Modifier.fillMaxSize()) {
        // Header Row
        Row(Modifier.fillMaxWidth().weight(0.2f)) {
            GridItem(text = "Header 1", weight = 0.5f, color = Color.LightGray)
            GridItem(text = "Header 2", weight = 0.5f, color = Color.LightGray)
        }

        // Main Content Rows
        Row(Modifier.fillMaxWidth().weight(0.8f)) {
            // Left Column
            Column(Modifier.weight(0.6f).fillMaxHeight()) {
                GridItem(text = "Item 1", weight = 0.4f, color = Color.White)
                GridItem(text = "Item 2", weight = 0.6f, color = Color.White)
            }

            // Right Column
            Column(Modifier.weight(0.4f).fillMaxHeight()) {
                GridItem(text = "Item 3", weight = 0.3f, color = Color.White)
                GridItem(text = "Item 4", weight = 0.7f, color = Color.White)
            }
        }
    }
}

@Composable
fun GridItem(text: String, weight: Float, color: Color) {
    Card(
        modifier = Modifier
            .weight(weight)
            .fillMaxSize()
            .padding(4.dp)
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(color),
            contentAlignment = Alignment.Center
        ) {
            Text(text = text, fontSize = 16.sp)
        }
    }
}


@Preview(showBackground = true)
@Composable
fun NestedGridLayoutPreview() {
    NestedGridLayout()
}

Explanation:

  • The outermost Column arranges content vertically.
  • Nested Row composables divide the space horizontally.
  • The weight modifier is crucial for allocating proportional space within each Row and Column.
  • GridItems get passed properties(Color, Text to display, Modifier for Styling purposes)

Method 3: Custom Layouts

For the most control and flexibility, creating a custom layout composable provides the means to position elements precisely within a grid.

Step 1: Implement the Custom Layout

Use Compose’s Layout composable to define the custom layout logic:


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.material3.Card
import androidx.compose.material3.Text
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.Constraints
import kotlin.random.Random
import androidx.compose.ui.graphics.Color

@Composable
fun CustomGridLayout(
    modifier: Modifier = Modifier,
    columnCount: Int = 2,
    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)
        }

        layout(
            width = constraints.maxWidth,
            height = placeables.sumOf { it.height }
        ) {
            var xPosition = 0
            var yPosition = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(x = xPosition, y = yPosition)
                xPosition += columnWidth
                if (xPosition >= constraints.maxWidth) {
                    xPosition = 0
                    yPosition += placeable.height
                }
            }
        }
    }
}

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

    CustomGridLayout(columnCount = 3, modifier = Modifier.padding(16.dp)) {
        items.forEach { item ->
            val randomColor = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256),255)
            Card(modifier = Modifier
                 .padding(4.dp),
                 ) {
                Box(contentAlignment = Alignment.Center,
                modifier = Modifier.background(randomColor).fillMaxWidth()) {
                    Text(text = item)
                }

            }
        }
    }
}

Explanation:

  • Layout is the core composable for creating custom layouts.
  • It provides access to measurables (the composables to be laid out) and constraints (size limitations).
  • The measure function determines the size of each item based on the provided constraints.
  • The placeRelative function positions the items within the layout.
  • In the layout block we define logic of X and Y co-ordinates of how the items would align given it meets/breaks contraints that it satisfies

Conclusion

Building complex grids in Jetpack Compose involves leveraging its flexible layout components and understanding when to use LazyVerticalGrid, nested layouts, or custom layouts. By mastering these techniques, you can create visually appealing, responsive, and highly customized UIs in your Android applications. Whether it’s creating a product gallery, a dashboard, or any other complex interface, Jetpack Compose offers the tools necessary to build sophisticated grid layouts.