Antes de entrar a fondo con el tema del estado, necesitamos una interfaz un poco más compleja para probar varios casos, así que vamos a explicar cómo funcionan las listas y los Grids en Jetpack Compose.
Ya vimos Column
y Row
en un artículo anterior, y te dije que existían unas variantes Lazy
que nos permitirían añadir un número indeterminado de elementos sin que esto penalizara el rendimiento.
Hoy te voy a explicar cómo funcionan.
Ahora que ya tenemos listas las celdas del listado gracias a la carga de imágenes con Coil, podemos ponernos manos a la obra.

¡Atención! Si quieres acceder más rápido a todo el contenido en vídeo, organizado, con contenido extra, soporte y muchas más sorpresas, puedes apuntarte gratis a mi formación Compose Expert y ver gratis el primer módulo de más de 3 horas con todo lo necesario para empezar.
LazyColumn
Esta seguramente va a ser la variante que más utilices, ya que nos define un listado vertical de elementos. La clásica vista de items uno detrás de otro.
Con LazyColumn
podemos definir un listado de items y mostrarlos por pantalla.
Si recuerdas toda la configuración que hacía falta para un RecyclerView
, esto te va a parecer de risa:
LazyColumn { items(100) { MediaItem() } }
Esto crea una lista de 100 elementos de tipo MediaItem
:
Vamos a crearnos un objeto para poder extraer la información y mostrar un listado un poco más realista:
data class MediaItem( val id: Int, val title: String, val thumb: String, val type: Type ) { enum class Type { PHOTO, VIDEO } }
Nos creamos un MediaItem
con la información necesaria para rellenar el listado, y con un tipo que identifica si debemos mostrar el icono del vídeo o no.
Nota: si, como yo, habías llamado al Composable, vas a tener que cambiarlo. Creo que tiene más sentido llamarlo
MediaListItem
, para que no se confunda con el estado.
Ahora voy a autogenerar una lista muy sencilla de 10 elementos:
fun getMedia() = (1..10).map { MediaItem( id = it, title = "Title $it", thumb = "https://lorempixel.com/400/400/people/$it/", type = if (it % 3 == 0) Type.VIDEO else Type.PHOTO ) }
Creo los 10 elementos a partir de unos índices. Para el tipo podemos generar algo aleatorio de muchas formas. En este caso, de cada 3 elementos 1 será vídeo.
Ahora vamos a usar esos elementos para generar la lista. Igual que existe un items()
que simplemente recibe el número de elementos, existe otra variante que acepta una lista como argumento. Vamos a usar esa:
LazyColumn { items(getMedia()) { MediaListItem(it) } }
El MediaListItem
aún no está preparado para aceptar un MediaItem
por argumento. Pero lo vamos a hacer ahora:
@Composable fun MediaListItem(mediaItem: MediaItem) { Column { Box( modifier = Modifier .height(200.dp) .fillMaxWidth() ) { Image( painter = rememberImagePainter( data = mediaItem.thumb ), contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) if (mediaItem.type == MediaItem.Type.VIDEO) { Icon( Icons.Default.PlayCircleOutline, contentDescription = null, modifier = Modifier .size(92.dp) .align(Alignment.Center), tint = Color.White ) } } Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colors.secondary) .padding(16.dp) ) { Text( text = mediaItem.title, style = MaterialTheme.typography.h6 ) } } }
Si te pierdes con alguno de estos puntos, te recomiendo que te apuntes a mi formación Compose Expert. En los vídeos del módulo gratuito se ha ido explicando todo para llegar hasta este punto.
De momento todo está muy bien, pero el diseño no “respira”. No hay espacio entre celdas.
¿Cómo podemos solucionarlo? Para ello tenemos dos parámetros en LazyColumn
:
LazyColumn( contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp) )
contentPadding
permite añadir un espaciado alrededor de todo el contenido. Un espaciado que además es “scrolleable” (cuando hacemos scroll desaparecen los de los bordes).
Por otro lado, con verticalArragement
podemos definir un espaciado entre elementos gracias a spacedBy()
. El resultado de lo anterior es este:
LazyRow
Todo lo que te he contado para LazyColumn
aplica directamente a LazyRow
, es el equivalente solo que posiciona los elementos en horizontal en vez de vertical.
LazyRow( contentPadding = PaddingValues(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { items(getMedia()) { MediaListItem(it) } }
Hay que cambiar algunas cosas en el item para que la fila se muestre correctamente, tal y como la ves en la imagen. Te lo dejo como ejercicio, aunque te dejo el código debajo:
@Composable fun MediaListItem(mediaItem: MediaItem) { Column ( modifier = Modifier.width(200.dp) ) { Box( modifier = Modifier.height(200.dp) ) { Image( painter = rememberImagePainter( data = mediaItem.thumb ), contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) if (mediaItem.type == MediaItem.Type.VIDEO) { Icon( Icons.Default.PlayCircleOutline, contentDescription = null, modifier = Modifier .size(92.dp) .align(Alignment.Center), tint = Color.White ) } } Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colors.secondary) .padding(16.dp) ) { Text( text = mediaItem.title, style = MaterialTheme.typography.h6 ) } } }
LazyVerticalGrid
Si te fijas, esto está muy bien para mostrar listados, ¿pero qué pasa si queremos mostrar varios elementos en una misma fila?
Necesitamos el LazyVerticalGrid
. En el momento de escribir este artículo, este componente es experimental, y no existe su homólogo LazyHorizontalGrid
.
Nuevamente, podemos volver a reutilizar lo que teníamos con LazyColumn
, solo que el verticalArrangement
no está disponible. Ya le buscaremos otra solución.
LazyVerticalGrid( cells = GridCells.Adaptive(150.dp), contentPadding = PaddingValues(4.dp) ) { items(getMedia()) { MediaListItem(it) } }
Para definir el número de elementos por fila es muy fácil. Tenemos el parámetro cells
, que gracias al objeto GridCells
nos permite dos opciones:
- Fixed: Una cantidad fija de elementos por fila
- Adaptive: Se proporciona un tamaño mínimo que debe tener la celda, y adapta el número de celdas en función de ese mínimo. Si ponemos 150dp y hay 290dp de espacio, solo habrá 1 celda de 290dp. Si hay 360dp, renderizará 2 celdas de 180dp.
Con el Adaptive
definido arriba, tendríamos este resultado tanto en portrait como en landscape:
¿Y qué pasa con el espacio entre celdas? No lo podemos hacer tan sencillo como antes, ¡pero que no cunda el pánico! Al fin y al cabo es un modifier. Añadiendo un parámetro modifier
al MediaListItem
como vimos en el artículo sobre modifiers, podemos hacerlo de forma sencilla:
LazyVerticalGrid( cells = GridCells.Adaptive(150.dp), contentPadding = PaddingValues(2.dp) ) { items(getMedia()) { MediaListItem( mediaItem = it, modifier = Modifier.padding(2.dp) ) } }
Puedes echar un vistazo al código completo en el commit específico del repositorio.
Ya tienes todas las herramientas para crear listas
Como ves, es muy sencilla hacer lo básico que teníamos con RecyclerView
en Jetpack Compose.
Sí que es cierto que hay muchas cosas que los RecyclerView
nos las facilitaban, y que con estos nuevos componentes no es tan sencillo.
Si quieres que cree algún otro vídeo con alguna de esas funcionalidades en particular, puedes dejármelo en los comentarios.

Apúntate ahora a Compose Expert y accede a más de 3h de contenido gratuito, soporte, extras y mucho más totalmente gratis.
En el siguiente artículo veremos cómo gestionar el estado en Jetpack Compose, ya que es bastante diferente de como estabas acostumbrado en el sistema clásico de vistas.
0 comentarios