Grids En Android

Las Grids en Android te permiten organizar colecciones de elementos en filas y columnas, es decir, cuadriculas de contenido.

Los ítems pueden ser distribuidos de forma vertical u horizontal y su distribución definirse uniforme o irregularmente (staggered grid) a lo largo del contenedor. Y por supuesto, este componente posee un scroll vertical u horizontal para la visualización de aquellos elementos ocultos por límite del tamaño de pantalla.

Tipos de Grids
Tipos de Grids

Normalmente las verás en casos de uso como galerías de imágenes, dashboards y catálogos de productos debido a que proporcionan excelente diseño para escaneo de información rica en contenido visual.

Con lo anterior dicho, en este tutorial aprenderás el paso a paso para crear cuadriculas con los componentes Lazy*Grid de Jetpack Compose en tus aplicativos Android. Verás como:

  • Crear una grid básica
  • Crear una grid escalonada
  • Personalizar espaciado de grilla e ítems
  • Añadir animaciones a los ítems

Configurar Un Proyecto En Android Studio Para Grids

Abre Android Studio y crea un nuevo proyecto con la plantilla Empty Activity de Compose y nómbralo Grids En Android.

Al igual que todos los tutoriales de Compose que hemos visto hasta el momento, este ejemplo requerirá de añadir como dependencias de Gradle.

Definir Actividad Principal

Abre la actividad creada por defecto MainActivity y renómbrala como GridsActivity. Luego reemplaza el código de la plantilla por el siguiente:

class GridsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            GridsTheme {
                GridsScreen()
            }
        }
    }
}

Como ves, dentro de setContent() invocaremos a nuestra función componible GridsScreen(), la cual tendrá la definición de los ejemplos que veremos.

Diseñar Pantalla Principal

Layout principal para ejemplo de Grids
Layout principal para ejemplo de Grids

La pantalla principal del ejemplo tiene el componente base Scaffold que nos permite añadir slots para Top App Bar y Floating Action Button.

Donde el contenido principal será la función componible que generemos para cada ejemplo de Grids. A partir de lo anterior, GridsScreen() se verá así:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GridsScreen() {

    var isConfigPanelOpen by rememberSaveable { mutableStateOf(false) }
    // Código omitido

    Scaffold(
        topBar = {
            TopBar(
                onConfigClick = { isConfigPanelOpen = true },
                verticalScrollBehavior = verticalScrollBehavior
            )
        },
        floatingActionButton = { Fab(viewModel::addItem) }
    ) {
        // Aquí invocas a los ejemplos que crearemos
    }

    if (isConfigPanelOpen) {
        ConfigPanel(
            // Código omitido
        )
    }
}

Más adelante verás que usaremos el Fab para añadir ítems a la cuadrícula con el fin de visualizar animaciones. Por otro lado, el action button en la app bar permite desplegar una Bottom Sheet (ConfigPanel) para visualizar los diferentes ejemplos, aun así, la construcción será omitida para enfocarnos en las grids.

Panel de configuración
Panel de configuración

Descargar Código Completo Desde GitHub

Por cuestiones de simplicidad, varios elementos ajenos a la explicación de Grids no serán comentados a lo largo del tutorial. Por lo que puedes ver el código completo desde el repositorio de la guía de UI en Github para que comprendas el panorama completo:

Crear Grid Vertical

Ejemplo de LazyVerticalGrid en Compose Android
Ejemplo de LazyVerticalGrid en Compose Android

Nuestro primer ejemplo será el caso básico de una cuadricula con dos columnas como ves en la ilustración anterior.

Crear Data Class De Estado

Si observas el diseño del ejemplo, necesitamos visualizar elementos con un nombre y el color de su background. Pero como estamos simulando datos hipotéticos y desconocemos que altura pudiesen llegar a tener los ítems, también necesitaremos una propiedad para esta característica.

La sentencia anterior la podemos materializar en una clase de datos llamada GridItemUiModel de la siguiente manera:

data class GridItemUiModel(
    val id: String,
    val title: String,
    val hexColor: Long = range32BitsColor.random()
) {

    companion object {
        private val range32BitsColor = (0x00000000..0xFFFFFFFF)

        var idCounter = 1

        fun newId() = idCounter++.toString()

        const val MIN_SIZE = 120
    }
}

Donde:

  • id: Identiticador del ítem. Su valor será generado a través de newId(), el cual incrementa un contador entero idCounter.
  • title: Título del ítem
  • hexColor: Long hexadecimal que representa el color del background
  • range32BitsColor: Rango de selección de color
  • MIN_SIZE: Constante con el tamaño en Dps del ítem

Con GridItemUiModel ya podemos crear una colección que represente el conjunto de elementos a desplegar en pantalla.

Crear ViewModel Para Grid

Normalmente la colección de ítems que deseamos proyectar viene de otra capa (datos o aplicación), por lo que necesitaremos crear un ViewModel que actué como intermediario para obtener dichos datos y formatearlos.

En nuestro ejercicio no contamos con dicho requerimiento por cuestiones de practicidad, no obstante, simularemos un origen del estado con una colección en memoria creada programáticamente. Con esto en mente, añade una nueva clase GridsViewModel al proyecto con este código:

class GridsViewModel : ViewModel() {
    private val _items = MutableStateFlow<List<GridItemUiModel>>(
        List(20) { newItem() }.reversed()
    )

    val items = _items.asStateFlow()

    private fun newItem(): GridItemUiModel {
        val itemId = GridItemUiModel.newId()
        return GridItemUiModel(itemId, itemId)
    }
}

En él declaramos a las propiedades _items e items para expresar mutabilidad e inmutabilidad del estado a través de flows. Aquí la lista inicia con veinte elementos GridItemUiModel, creada a partir de la función constructora List().

Normalmente no necesitamos de la propiedad mutable, pero como más adelante añadiremos y removeremos elementos, cobra sentido en este caso.

Crear Componente Para Item De Grilla

Nuestros ítems tienen como contenedor principal un Card delineada que envuelve un componente de texto. De modo que al interior de un nuevo archivo llamado GridItem.kt, añadimos la función GridItem():

@Composable
internal fun GridItem(
    modifier: Modifier = Modifier,
    item: GridItemUiModel,
    onClick: () -> Unit
) {
    OutlinedCard(
        modifier = modifier.clickable(onClick = onClick),
        colors = CardDefaults.cardColors(containerColor = Color(item.hexColor)),
    ) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            Text(
                text = item.title,
                style = MaterialTheme.typography.titleLarge,
                modifier = Modifier.Companion.padding(16.dp)
            )
        }
    }
}

@Composable
@Preview
private fun GridItemPreview() {
    GridItem(
        modifier = Modifier.size(200.dp),
        item = GridItemUiModel("1", "1"),
        onClick = {}
    )
}

Como ves, el componente recibe un modificador para permitir a los padres configurar los aspectos de UI del ítem (tenemos varios ejemplos que usan diferentes modificadores width() y height()).

También está el parámetro GridItemUiModel nos permite vincular las propiedades a la vista.

Y por último la lambda onClick que permite asociar acciones cuando ítem sea clickado.

Usar Componente LazyVerticalGrid De Compose

La función componible que representa una grid vertical en Compose es LazyVerticalGrid(). Esta requiere de dos parámetros obligatorios para su invocación:

  • columns: Describe al cantidad y tamaño de las columnas de la cuadrícula. Es de tipo GridCells, el cual provee tres modos
    • Fixed: Define un número de columnas fijas cuyo ancho será el ancho del padre dividido la cantidad de las mismas
    • Adaptive: Dividirá el ancho de la lista entre el ancho que pases como parámetro para calcular el número de columnas. El espacio restante es distribuido equitativamente en las columnas definidas
    • FixedSize: Similar a Adaptive, solo que el ancho restante no es distribuido
  • content: Una lambda con recibidor del tipo LazyGridScope, que al igual que LazyColumn, provee métodos items() e item() para construir tu cuadrícula

A partir de la descripción técnica anterior, implementemos nuestra primera Grid.

GridCells.Fixed

Ejemplo de LazyVerticalGrid con GridCells.Fixed
Ejemplo de LazyVerticalGrid con GridCells.Fixed con valores 2, 3 y 4.

Añade un nuevo archivo llamado VerticalGridExample.kt y declara una función @Composable con el mismo nombre. La idea es recibir como parámetro el modificador, una lista de elementos GridItemUiModel y la lambda que procesa el click sobre un ítem.

Adicionalmente, usaremos GridCells.Fixed con el argumento 2 para representar dos columnas en la cuadrícula:

@Composable
fun VerticalGridExample(
    items: List<GridItemUiModel>,
    modifier: Modifier = Modifier,   
    onGridItemClick: (GridItemUiModel) -> Unit
) {
    LazyVerticalGrid(
        modifier = modifier,
        columns = GridCells.Fixed(2), // ← Número de columnas
    ) {
        items(items = items) {
            GridItem(
                modifier = Modifier.height(GridItemUiModel.MIN_SIZE.dp),
                item = it,
                onClick = { onGridItemClick(it) }
            )
        }
    }
}

GridCells.Adaptive

Ejemplo de LazyVerticalGrid con GridCells.Adaptive
Ejemplo de LazyVerticalGrid con GridCells.Adaptive con valores de 103, 154, 180 y 206dp

Si reemplazas el valor de columns del código por el tipo GridCells.Adaptive y un argumento de 120.dp, la función LazyVerticalGrid distribuirá los elementos en 3 columnas.

@Composable
fun VerticalGridExample(
    //...
) {
    LazyVerticalGrid(
        modifier = modifier,
        columns = GridCells.Adaptive(120.dp), // ← Ancho de columna
    ) {
        // ..
    }
}

En este ejemplo usamos el emulador Pixel 9 con 412dp de ancho. Esto significa que 412/120=3.43, lo que nos deja 3 columnas. La porción restante de 52dp es distribuida entre las columnas, dejándolas con un tamaño aproximado de 137.3dp.

GridCells.FixedSize

Ejemplo de LazyVerticalGrid con GridCells.FixedSize
Ejemplo de LazyVerticalGrid con GridCells.FixedSize. Nota como el espacio restante se mantiene

Ahora fijemos el tamaño de las columnas a 206dp con GridCells.FixedSize para experimentar el resultado:

@Composable
fun VerticalGridExample(
    //...
) {
    LazyVerticalGrid(
        modifier = modifier,
        columns = GridCells.FixedSize(206.dp), // ← Ancho de columna
    ) {
        // ..
    }
}

Si observas la captura de pantalla del emulador, el resultado nos da 1 columna, pero esta vez el espacio restante (206dp) no es distribuido, el espacio queda libre.

Crear Grid Horizontal

Ejemplo de LazyHorizontalGrid
Ejemplo de LazyHorizontalGrid con 3 filas

Si necesitas crear una Grid horizontal, usa el componente LazyHorizontalGrid(). Su firma es exactamente igual que la de LazyVerticalGrid, con la diferencia de que se especifica el uso de filas (rows) en lugar de columnas.

Añade un nuevo archivo HorizontalGridExample.kt y codifica una cuadrilla con 3 filas:

@Composable
fun HorizontalGridExample(
    items: List<GridItemUiModel>,
    modifier: Modifier = Modifier,
    onGridItemClick: (GridItemUiModel) -> Unit = {}
) {
    LazyHorizontalGrid(
        modifier = modifier.height(412.dp),
        rows = GridCells.Fixed(3)
        content = {
            items(items = items) {
                GridItem(
                    modifier = Modifier.width(GridItemUiModel.MIN_SIZE.dp),
                    item = it,
                    onClick = { onGridItemClick(it) }
                )
            }
        }
    )
}

En el caso de usar GridCells.Adaptive y GridsCells.FixedSize, el espacio será distribuido horizontalmente de la misma forma que percibimos anteriormente.

Crear Staggered Grid Vertical

Ejemplo de LazyVerticalStaggeredGrid con StaggeredGridCells.Fixed(3)
Ejemplo de LazyVerticalStaggeredGrid con StaggeredGridCells.Fixed(3)

Una Staggered Grid es una cuadricula que organiza a sus elementos de forma escalonada sobre el contenedor. Esto quiere decir que sus ítems pueden tener diferentes alturas o anchos con el fin de producir un efecto visual de mosaico.

Para crear una cuadricula escalonada vertical usamos el componente LazyVerticalStaggeredGrid. Este recibe casi los mismos parámetros que su forma uniforme, la diferencia radica en que las interfaces para los tipos varían. El parámetro de contenido usa a LazyStaggeredGridScope y las columnas a StaggeredGridCells.

Con esto en mente, crea un nuevo archivo llamado VerticalStaggeredGridExample.kt y codifica una cuadricula con tres columnas a través de StaggeredGridCells.Fixed:

@Composable
fun VerticalStaggeredGridExample(
    items: List<GridItemUiModel>,
    modifier: Modifier = Modifier,
    onGridItemClick: (GridItemUiModel) -> Unit = {}
) {
    LazyVerticalStaggeredGrid(
        modifier = modifier,
        columns = StaggeredGridCells.Fixed(3)
    ) {
        items(items = items) {
            GridItem(
                modifier = Modifier.height(it.sizeForStaggered.dp),
                item = it,
                onClick = { onGridItemClick(it) }
            )
        }
    }
}

Para simular las diferentes alturas de nuestros ítems hemos usado una propiedad inexistente GridItemUiModel.sizeForStaggered. Así que añadámosla para ver su propósito:

data class GridItemUiModel(
    //...
) {
    val sizeForStaggered = MIN_SIZE * rangeForRandom.random()

    companion object {
        private val rangeForRandom = 1..3
        //...
    }
}

Su declaración se basa en la elección aleatoria de tres posibles tamaños: 1x, 2x y 3x del valor de MIN_SIZE. De esta forma podemos simular que existe contenido con diferentes longitudes que obliga expandir al ítem de la grilla para mostrarlo

Crear Staggered Grid Horizontal

Ejemplo de LazyHorizontalStaggeredGrid con StaggeredGridCells.Fixed(3)
Ejemplo de LazyHorizontalStaggeredGrid con argumento StaggeredGridCells.Fixed(3) en rows

En contraste de la vertical, una cuadricula escalonada horizontal aplicará la distribución de los ítems desde su ancho. El componente de Compose que la representa es LazyHorizontalStaggeredGrid. Su invocación es similar a la vertical, solo que no tomamos a columns si no rows.

Ilustremos su uso creando una staggered grid horizontal con cuatro filas en un nuevo archivo HorizontalStaggeredGridExample.kt:

@Composable
fun HorizontalStaggeredGridExample(
    items: List<GridItemUiModel>,
    modifier: Modifier = Modifier,
    onGridItemClick: (GridItemUiModel) -> Unit = {}
) {
    LazyHorizontalStaggeredGrid(
        modifier = modifier.height(412.dp),
        rows = StaggeredGridCells.Fixed(3)
    ) {
        items(items = items) {
            GridItem(
                modifier = Modifier.width(it.sizeForStaggered.dp),
                item = it,
                onClick = { onGridItemClick(it) }
            )
        }
    }
}

Esta vez accedemos a sizeForStaggered pero para variar el ancho de los ítems a través del modificador width().

Personalizar Grid

Ajustar Padding Del Contenido

Ejemplo de parámetro contentPadding con valores del rango [0,16]dp
Ejemplo de parámetro contentPadding con valores del rango [0,16]dp

Hasta el momento todos los ejemplos creados están desplegados hasta los límites del contenedor de la Grid. Situación que no es ideal, ya que necesitamos proporcionar espacios para promover la organización visual en nuestras pantallas.

Afortunadamente y como habrás explorado, los componentes Compose de cuadriculas traen consigo un parámetro llamado contentPadding. Este representa el relleno de la cuadricula, es decir, el espacio entre el borde y los ítems.

Por ejemplo, añadamos 16 Dps de padding a nuestro ejemplo VerticalGridExample:

@Composable
fun VerticalGridExample(
    //...
) {
    LazyVerticalGrid(
        contentPadding = PaddingValues(all = 16.dp)
    ) {
        // ..
    }
}

Tal como se aprecia, contentPadding es del tipo PaddingValues, el cual puede ser construido con varias funciones especificando los bordes del contenedor. Nosotros usamos la firma con all para asignar el mismo valor a los cuatro bordes, pero también puedes usar PaddingValues(horizontal, vertical) o PadddingValues(start, top, end, bottom).

Ajustar Espaciado Entre Items

También es posible añadir espacio vertical y horizontal entre los ítems de las Grids a partir de dos parámetros en los componentes.

Ejemplo de espaciado vertical y horizontal en Grid
Ejemplo de espaciado vertical y horizontal en Grid

Para las versiones básicas de las cuadriculas empleamos a verticalArrangement para el espaciado vertical y horizontalArrangement para el horizontal. Y el argumento a pasar lo construimos con la función Arrangement.spacedBy(), la cual recibe los Dps del espacio. Por ejemplo:

@Composable
fun VerticalGridExample(
    //...
) {
    LazyVerticalGrid(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // ..
    }
}

En el caso de las versiones escalonadas, la cuadricula vertical en lugar de verticalArrangement usa un parámetro verticalItemSpacing. Similar sucede con la horizontal, no tiene a horizontalArrangement si no a horizontalItemSpacing.

Ejemplo de espaciado de ítems en grilla escalonada horizontal
Ejemplo de espaciado de ítems en grilla escalonada horizontal

Por ejemplo, pasemos 8 Dps en ambos tipos de staggered grid:

@Composable
fun VerticalStaggeredGridExample(
    //...
) {
    LazyVerticalStaggeredGrid(
        //...
        verticalItemSpacing = 8.dp,
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        //...
    }
}

@Composable
fun HorizontalStaggeredGridExample(
    //...
) {
    LazyHorizontalStaggeredGrid(
        //...
        horizontalItemSpacing = 8.dp,
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        //...
    }
}

Animar Items De Grid

Actualmente si la lista de ítems que tenemos cambia su tamaño, las cuadriculas creadas como ejemplo no mostrarán una animación para representar la actualización visual del contenido. Sin embargo, es fácil añadir una animación de Fade In y Fade Out con el modificador animateItem().

Para probar las animaciones necesitaremos:

  • Una clave distintiva a cada ítem de la cuadricula al construirla con el DSL que pasamos como argumento en content
  • Un método addItem() en el view model para añadir un ítem a la cuadrilla
  • Un método removeItem() en el view model para remover un ítem a la cuadrilla

Resolvamos estas condiciones.

Vincular Key A Item De Grid

El modificador animateItem() requiere que pasemos una clave única para habilitar las animaciones en nuestra Grid. Debido a que nuestra clase GridItemUiModel contiene una propiedad id, este es un buen valor para pasar en nuestras grids de ejemplo:

@Composable
fun VerticalGridExample(
    //...
) {
    LazyVerticalGrid(
        //...
    ) {
        items(items = items, key = { it.id } ) { // ← Vinculación de clave
            GridItem(
                modifier = Modifier
                    .animateItem() // ← Modificador para animar elemento
                    .height(GridItemUiModel.MIN_SIZE.dp),
                //...
            )
        }
    }
}

Como puedes notar, pasamos el id en el parámetro key de la función items() donde construimos cualquiera de nuestras grillas. Con ello las animaciones son habilitadas y ahora debemos modificar nuestro estado para observar el resultado.

Añadir Item A Grid

Ejemplo de animateItem() al agregar un elemento a la Grid
Ejemplo de animateItem() al agregar un elemento a la Grid

La representación de la creación de un nuevo elemento la representaremos a través de un método addItem() desde GridsViewModel:

class GridsViewModel : ViewModel() {
    //...

    fun addItem() {
        _items.update {
            listOf(newItem()) + it
        }
    }

    private fun newItem(): GridItemUiModel {
        val itemId = GridItemUiModel.newId()
        return GridItemUiModel(itemId, itemId)
    }
}

Lo que hacemos es reemplazar el valor actual de _items por una nueva lista, resultado de sumar el nuevo elemento y la lista actual.

Con esto resuelto, vamos al Floating Action Button de GridsScreen y le pasamos la referencia del método additem() a su parámetro onClick.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GridsScreen(viewModel: GridsViewModel = viewModel()) {
    val gridItems by viewModel.items.collectAsStateWithLifecycle()

    Scaffold(
        //...
        floatingActionButton = { Fab(viewModel::addItem) } // ← Vinculo a view model
    ) {
        //...
    }

    //...
}

Luego de que el código anterior sea implementado, podrás obtener el resultado observado en la animación mostrada al inicio. El nuevo ítem añadido aparecerá suavemente y los demás serán reordenados.

Remover Item De Grid

Ejemplo de animación al remover un elemento de una Grid
Ejemplo de animación al remover un elemento de una Grid cuando este es clickeado

Ahora en GridsViewModel creamos el método removeItem() para remover un elemento del estado actual. Crear una nueva lista con un elemento menos se logra usando el operador menos -, donde el minuendo es la lista y el sustraendo es el GridItemUiModel a eliminar.

class GridsViewModel : ViewModel() {
    //...

    fun removeItem(item: GridItemUiModel) {
        _items.update { it - item }
    }
}

En virtud de que la eliminación se da cuando tocamos los ítems de la cuadrícula, entonces añadimos un parámetro a las funciones de ejemplo llamado onGridItemClick. Dicha lambda será pasada en onClick de GridItem():

@Composable
fun VerticalGridExample(
    //...
    onGridItemClick: (GridItemUiModel) -> Unit // ← Lambda al tocar ítem
) {
    LazyVerticalGrid(
        //...
    ) {
        items(items = items, key = { it.id }) {
            GridItem(
                //...
                onClick = { onGridItemClick(it) } // ← Vinculación al ítem
            )
        }
    }
}

Por lo que desde GridsScreen() pasas el valor al componente de grilla con la referencia GridsViewModel::removeItem():

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GridsScreen(viewModel: GridsViewModel = viewModel()) {
    //...

    Scaffold(
        //...
    ) {
        LazyVerticalGrid(            
            //...
            onGridItemClick = viewModel::removeItem
        )
    }

    //...
}

Y como viste en el gif del inicio, cada que toques un ítem de la grilla experimentarás como el elemento eliminado se desvanece y los demás son desplazados para reordenar el espacio

Conclusión

Jetpack Compose proporciona un conjunto robusto de componentes para construir grids personalizables mediante LazyVerticalGrid, LazyHorizontalGrid, LazyVerticalStaggeredGrid y LazyHorizontalStaggeredGrid. A través de estas APIs, es posible definir estructuras uniformes o escalonadas, configurar el número y tipo de celdas con GridCells, controlar el espaciado entre elementos y aplicar padding interno al contenedor.

Además, la incorporación de animaciones mediante animateItem() permite gestionar cambios en la colección de forma visualmente fluida, mejorando la experiencia del usuario. El uso de claves únicas por ítem y la gestión de estado desde el ViewModel aseguran un comportamiento predecible y fácil de mantener.

En resumen, Compose simplifica considerablemente la creación de interfaces en grid con alto grado de personalización, manteniendo al mismo tiempo una arquitectura limpia y escalable.

¿Te fue útil este ejemplo? Envíame un mensaje en mi servidor de Discord o comparte este artículo en redes.

¿Estás Creando Una App De tareas?

Te comparto una plantilla Android profesional con arquitectura limpia, interfaz moderna y funcionalidades listas para usar. Ideal para acelerar tu desarrollo.

Banner de plantilla de tareas Android

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!