Jetpack Compose GridView

Jetpack Compose GridView, implemented through LazyVerticalGrid, is a composable function that allows you to create grid-based layouts where items are lazily loaded. This means only the visible items are rendered, resulting in improved performance and reduced memory consumption - a critical optimization for mobile applications that need to display large datasets.

The LazyVerticalGrid is part of Jetpack Compose's foundation library and serves as the modern replacement for the traditional Android GridView widget. It follows Compose's declarative approach to UI development, making grid layouts more intuitive to implement and maintain.

Key Features of Jetpack Compose GridView

Before diving into implementation, let's understand the key features that make LazyVerticalGrid an essential tool for building grid layouts:

  • Lazy Loading: Only renders visible items, improving performance for large datasets
  • Fixed or Adaptive Columns: Supports both fixed number of columns or adaptive columns based on available space
  • Customizable Item Spans: Items can span multiple columns using GridItemSpan
  • Arrangement Control: Offers vertical and horizontal arrangement customization
  • Content Padding: Allows adding padding around the entire grid content
  • Scrolling Behavior: Built-in scrolling with customizable fling behavior

Implementing Basic Jetpack Compose GridView

Let's start with a simple implementation of Jetpack Compose GridView using LazyVerticalGrid:

@Composable  
fun SimpleGridView() {  
    val items = (1..20).toList()  
      
    LazyVerticalGrid(  
        columns = GridCells.Fixed(2),  
        contentPadding = PaddingValues(8.dp),  
        horizontalArrangement = Arrangement.spacedBy(8.dp),  
        verticalArrangement = Arrangement.spacedBy(8.dp)  
    ) {  
        items(items) { item ->  
            GridItem(item)  
        }  
    }  
}  
  
@Composable  
fun GridItem(item: Int) {  
    Card(  
        modifier = Modifier  
            .fillMaxWidth()  
            .height(120.dp),  
        elevation = CardDefaults.cardElevation(4.dp)  
    ) {  
        Box(  
            modifier = Modifier.fillMaxSize(),  
            contentAlignment = Alignment.Center  
        ) {  
            Text(  
                text = "Item $item",  
                fontSize = 16.sp,  
                fontWeight = FontWeight.Bold  
            )  
        }  
    }  
}  

In this example, we've created a simple grid with two fixed columns and 20 items. Each item is represented by a Card with centered text.

Understanding GridCells Types

The LazyVerticalGrid component in Jetpack Compose offers two main types of GridCells:

1. Fixed Columns

The GridCells.Fixed(n) parameter creates a grid with a fixed number of columns. Each column will have equal width, calculated by dividing the available width by the number of columns.

LazyVerticalGrid(  
    columns = GridCells.Fixed(3),  
    // other parameters  
) {  
    // content  
}  

This creates a grid with exactly 3 columns, each taking up 1/3 of the available width.

2. Adaptive Columns

The GridCells.Adaptive(minSize) parameter creates a grid with as many columns as possible while ensuring each column has at least the specified minimum width.

LazyVerticalGrid(  
    columns = GridCells.Adaptive(150.dp),  
    // other parameters  
) {  
    // content  
}  

This allows the grid to adapt to different screen sizes. On wider screens, more columns will be displayed, while on narrower screens, fewer columns will be shown.

Creating a Grid with Images

Let's implement a more practical example with images - a common use case for grid layouts:

data class PhotoItem(  
    val id: Int,  
    @DrawableRes val imageResId: Int,  
    val title: String  
)  
  
@Composable  
fun PhotoGrid(photos: List<PhotoItem>) {  
    LazyVerticalGrid(  
        columns = GridCells.Adaptive(150.dp),  
        contentPadding = PaddingValues(8.dp),  
        horizontalArrangement = Arrangement.spacedBy(8.dp),  
        verticalArrangement = Arrangement.spacedBy(8.dp)  
    ) {  
        items(photos) { photo ->  
            PhotoGridItem(photo)  
        }  
    }  
}  
  
@Composable  
fun PhotoGridItem(photo: PhotoItem) {  
    Card(  
        modifier = Modifier  
            .fillMaxWidth()  
            .aspectRatio(0.8f),  
        elevation = CardDefaults.cardElevation(4.dp)  
    ) {  
        Column {  
            Image(  
                painter = painterResource(id = photo.imageResId),  
                contentDescription = photo.title,  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .weight(1f),  
                contentScale = ContentScale.Crop  
            )  
              
            Text(  
                text = photo.title,  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .padding(8.dp),  
                maxLines = 1,  
                overflow = TextOverflow.Ellipsis,  
                style = MaterialTheme.typography.bodyMedium  
            )  
        }  
    }  
}  

This example creates a photo gallery with adaptive columns. Each photo item consists of an image and a title, displayed in a card layout.

Working with Spans in GridView

Sometimes, you might want certain items to span multiple columns. Jetpack Compose GridView supports this through the span parameter in the items function:

LazyVerticalGrid(  
    columns = GridCells.Fixed(3),  
    // other parameters  
) {  
    item(  
        span = { GridItemSpan(3) }  // Span all 3 columns  
    ) {  
        // Header spanning all columns  
        Text(  
            text = "Featured Items",  
            style = MaterialTheme.typography.headlineMedium,  
            modifier = Modifier  
                .fillMaxWidth()  
                .padding(16.dp)  
        )  
    }  
      
    items(  
        items = dataList,  
        span = { item ->  
            // Some logic to determine span  
            if (item.isFeatured) {  
                GridItemSpan(2)  // Span 2 columns for featured items  
            } else {  
                GridItemSpan(1)  // Regular items take 1 column  
            }  
        }  
    ) { item ->  
        // Item content  
    }  
}  

This feature is particularly useful for creating headers, footers, or highlighting certain items by making them larger.

Handling Nested Scrolling with GridView

A common challenge when working with LazyVerticalGrid is integrating it within other scrollable containers like a Column with the verticalScroll modifier. Since both the Column and LazyVerticalGrid would scroll in the same direction, this creates a conflict.

One approach to solve this is using the heightIn modifier to set a maximum height for the LazyVerticalGrid:

Column(  
    modifier = Modifier  
        .fillMaxSize()  
        .verticalScroll(rememberScrollState())  
) {  
    Text(  
        text = "Header Section",  
        style = MaterialTheme.typography.headlineMedium,  
        modifier = Modifier.padding(16.dp)  
    )  
      
    LazyVerticalGrid(  
        columns = GridCells.Fixed(2),  
        modifier = Modifier  
            .heightIn(max = 500.dp),  // Set a maximum height  
        contentPadding = PaddingValues(8.dp)  
    ) {  
        items(gridItems) { item ->  
            GridItem(item)  
        }  
    }  
      
    Text(  
        text = "Footer Section",  
        style = MaterialTheme.typography.bodyLarge,  
        modifier = Modifier.padding(16.dp)  
    )  
}  

Alternatively, for more complex layouts, you can implement a custom grid view using Row and Column composables.

Creating a Custom Grid with LazyColumn

If LazyVerticalGrid doesn't meet your specific requirements, you can create a custom grid implementation using LazyColumn and Row:

@Composable  
fun <T> CustomGridView(  
    items: List<T>,  
    columnsCount: Int,  
    itemContent: @Composable (T) -> Unit  
) {  
    val chunkedItems = items.chunked(columnsCount)  
      
    LazyColumn(  
        contentPadding = PaddingValues(8.dp),  
        verticalArrangement = Arrangement.spacedBy(8.dp)  
    ) {  
        items(chunkedItems) { rowItems ->  
            Row(  
                horizontalArrangement = Arrangement.spacedBy(8.dp)  
            ) {  
                rowItems.forEach { item ->  
                    Box(  
                        modifier = Modifier.weight(1f)  
                    ) {  
                        itemContent(item)  
                    }  
                }  
                  
                // Fill empty spaces if the last row is not complete  
                repeat(columnsCount - rowItems.size) {  
                    Spacer(modifier = Modifier.weight(1f))  
                }  
            }  
        }  
    }  
}  

This custom implementation uses LazyColumn to create rows and places items within each row using Row and Box composables.

Complete Example: Product Grid with Categories

Let's put everything together with a complete example of a product grid with category headers:

data class Product(  
    val id: Int,  
    val name: String,  
    val category: String,  
    @DrawableRes val imageRes: Int,  
    val price: Double,  
    val isFeatured: Boolean = false  
)  
  
@Composable  
fun ProductGridScreen(products: List<Product>) {  
    val groupedProducts = products.groupBy { it.category }  
      
    LazyVerticalGrid(  
        columns = GridCells.Adaptive(160.dp),  
        contentPadding = PaddingValues(12.dp),  
        horizontalArrangement = Arrangement.spacedBy(12.dp),  
        verticalArrangement = Arrangement.spacedBy(12.dp)  
    ) {  
        groupedProducts.forEach { (category, categoryProducts) ->  
            // Category header spanning all columns  
            item(span = { GridItemSpan(maxLineSpan) }) {  
                CategoryHeader(category)  
            }  
              
            // Products in this category  
            items(  
                items = categoryProducts,  
                span = { product ->  
                    if (product.isFeatured) GridItemSpan(2) else GridItemSpan(1)  
                }  
            ) { product ->  
                ProductCard(product)  
            }  
        }  
    }  
}  
  
@Composable  
fun CategoryHeader(category: String) {  
    Text(  
        text = category,  
        style = MaterialTheme.typography.headlineSmall,  
        modifier = Modifier  
            .fillMaxWidth()  
            .background(MaterialTheme.colorScheme.surfaceVariant)  
            .padding(horizontal = 16.dp, vertical = 8.dp)  
    )  
}  
  
@Composable  
fun ProductCard(product: Product) {  
    Card(  
        modifier = Modifier  
            .fillMaxWidth()  
            .aspectRatio(if (product.isFeatured) 1.5f else 0.8f),  
        elevation = CardDefaults.cardElevation(4.dp)  
    ) {  
        Column {  
            Box(  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .weight(1f)  
            ) {  
                Image(  
                    painter = painterResource(id = product.imageRes),  
                    contentDescription = product.name,  
                    modifier = Modifier.fillMaxSize(),  
                    contentScale = ContentScale.Crop  
                )  
                  
                if (product.isFeatured) {  
                    Surface(  
                        color = MaterialTheme.colorScheme.primaryContainer,  
                        modifier = Modifier  
                            .padding(8.dp)  
                            .align(Alignment.TopEnd)  
                    ) {  
                        Text(  
                            text = "Featured",  
                            modifier = Modifier.padding(4.dp),  
                            style = MaterialTheme.typography.labelSmall  
                        )  
                    }  
                }  
            }  
              
            Column(  
                modifier = Modifier.padding(8.dp)  
            ) {  
                Text(  
                    text = product.name,  
                    style = MaterialTheme.typography.bodyMedium,  
                    maxLines = 1,  
                    overflow = TextOverflow.Ellipsis  
                )  
                  
                Text(  
                    text = "$${product.price}",  
                    style = MaterialTheme.typography.bodySmall,  
                    fontWeight = FontWeight.Bold,  
                    color = MaterialTheme.colorScheme.primary  
                )  
            }  
        }  
    }  
}  

Full Example to Run

Here's a complete, runnable example incorporating all the concepts discussed:

import androidx.compose.foundation.Image  
import androidx.compose.foundation.background  
import androidx.compose.foundation.layout.*  
import androidx.compose.foundation.lazy.grid.*  
import androidx.compose.material3.*  
import androidx.compose.runtime.Composable  
import androidx.compose.ui.Alignment  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.layout.ContentScale  
import androidx.compose.ui.res.painterResource  
import androidx.compose.ui.text.font.FontWeight  
import androidx.compose.ui.text.style.TextOverflow  
import androidx.compose.ui.unit.dp  
import androidx.compose.ui.unit.sp  
import androidx.annotation.DrawableRes  
  
@Composable  
fun JetpackComposeGridViewDemo() {  
    // Sample data for our grid  
    val products = listOf(  
        Product(1, "Smartphone", "Electronics", R.drawable.smartphone, 599.99, true),  
        Product(2, "Laptop", "Electronics", R.drawable.laptop, 1299.99),  
        Product(3, "Headphones", "Electronics", R.drawable.headphones, 199.99),  
        Product(4, "T-shirt", "Clothing", R.drawable.tshirt, 24.99),  
        Product(5, "Jeans", "Clothing", R.drawable.jeans, 49.99, true),  
        Product(6, "Sneakers", "Footwear", R.drawable.sneakers, 89.99),  
        Product(7, "Watch", "Accessories", R.drawable.watch, 149.99),  
        Product(8, "Backpack", "Accessories", R.drawable.backpack, 59.99),  
        Product(9, "Sunglasses", "Accessories", R.drawable.sunglasses, 79.99, true)  
    )  
      
    Surface(  
        modifier = Modifier.fillMaxSize(),  
        color = MaterialTheme.colorScheme.background  
    ) {  
        Column {  
            TopAppBar(  
                title = { Text("Jetpack Compose GridView") },  
                colors = TopAppBarDefaults.topAppBarColors(  
                    containerColor = MaterialTheme.colorScheme.primaryContainer  
                )  
            )  
              
            ProductGridScreen(products)  
        }  
    }  
}  
  
data class Product(  
    val id: Int,  
    val name: String,  
    val category: String,  
    @DrawableRes val imageRes: Int,  
    val price: Double,  
    val isFeatured: Boolean = false  
)  
  
@Composable  
fun ProductGridScreen(products: List<Product>) {  
    val groupedProducts = products.groupBy { it.category }  
      
    LazyVerticalGrid(  
        columns = GridCells.Adaptive(160.dp),  
        contentPadding = PaddingValues(12.dp),  
        horizontalArrangement = Arrangement.spacedBy(12.dp),  
        verticalArrangement = Arrangement.spacedBy(12.dp)  
    ) {  
        groupedProducts.forEach { (category, categoryProducts) ->  
            // Category header spanning all columns  
            item(span = { GridItemSpan(maxLineSpan) }) {  
                CategoryHeader(category)  
            }  
              
            // Products in this category  
            items(  
                items = categoryProducts,  
                span = { product ->  
                    if (product.isFeatured) GridItemSpan(2) else GridItemSpan(1)  
                }  
            ) { product ->  
                ProductCard(product)  
            }  
        }  
    }  
}  
  
@Composable  
fun CategoryHeader(category: String) {  
    Text(  
        text = category,  
        style = MaterialTheme.typography.headlineSmall,  
        modifier = Modifier  
            .fillMaxWidth()  
            .background(MaterialTheme.colorScheme.surfaceVariant)  
            .padding(horizontal = 16.dp, vertical = 8.dp)  
    )  
}  
  
@Composable  
fun ProductCard(product: Product) {  
    Card(  
        modifier = Modifier  
            .fillMaxWidth()  
            .aspectRatio(if (product.isFeatured) 1.5f else 0.8f),  
        elevation = CardDefaults.cardElevation(4.dp)  
    ) {  
        Column {  
            Box(  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .weight(1f)  
            ) {  
                Image(  
                    painter = painterResource(id = product.imageRes),  
                    contentDescription = product.name,  
                    modifier = Modifier.fillMaxSize(),  
                    contentScale = ContentScale.Crop  
                )  
                  
                if (product.isFeatured) {  
                    Surface(  
                        color = MaterialTheme.colorScheme.primaryContainer,  
                        modifier = Modifier  
                            .padding(8.dp)  
                            .align(Alignment.TopEnd)  
                    ) {  
                        Text(  
                            text = "Featured",  
                            modifier = Modifier.padding(4.dp),  
                            style = MaterialTheme.typography.labelSmall  
                        )  
                    }  
                }  
            }  
              
            Column(  
                modifier = Modifier.padding(8.dp)  
            ) {  
                Text(  
                    text = product.name,  
                    style = MaterialTheme.typography.bodyMedium,  
                    maxLines = 1,  
                    overflow = TextOverflow.Ellipsis  
                )  
                  
                Text(  
                    text = "$${product.price}",  
                    style = MaterialTheme.typography.bodySmall,  
                    fontWeight = FontWeight.Bold,  
                    color = MaterialTheme.colorScheme.primary  
                )  
            }  
        }  
    }  
}  

Summary

The Jetpack Compose GridView, implemented through LazyVerticalGrid, offers a powerful and flexible way to create grid layouts in your Android applications. With its lazy loading capabilities, customizable column arrangements, and support for item spans, it provides all the tools needed to build sophisticated grid-based UIs.

By understanding the various parameters and customization options available with LazyVerticalGrid, you can create performant and visually appealing grid layouts that adapt to different screen sizes and orientations.

Whether you're building a photo gallery, product catalog, or any other grid-based UI, Jetpack Compose GridView provides a modern, declarative approach that simplifies implementation while maintaining excellent performance.