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.
Before diving into implementation, let's understand the key features that make LazyVerticalGrid an essential tool for building grid layouts:
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.
The LazyVerticalGrid component in Jetpack Compose offers two main types of GridCells:
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.
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.
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.
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.
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.
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.
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
)
}
}
}
}
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
)
}
}
}
}
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.