Listas y Grids en Jetpack Compose
Antonio Leiva

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.

Quizá también te interese…

Temas, colores, tipografías y formas en Jetpack Compose

Temas, colores, tipografías y formas en Jetpack Compose

Si vienes del sistema clásico de vistas, recordarás que toda la definición de temas se hacía de una forma bastante tediosa a través de styles en XML. Si odias tu vida (o estás en una App donde mezclas XMLs y Jetpack Compose), aún puedes seguir usando esos temas...

Usando Cards de Material Design en Jetpack Compose

Usando Cards de Material Design en Jetpack Compose

Tenemos ya una App de lo más resultona después de todos los artículos que hemos visto hasta ahora. Puedes encontrarlos todos bien ordenaditos de forma gratuita en el área privada de Compose Expert. https://youtu.be/iZiXpWRIl3U Pero antes de finalizar nuestro camino,...

Navegación en Jetpack Compose con Navigation Compose

Navegación en Jetpack Compose con Navigation Compose

Jetpack Compose es un cambio de paradigma enorme en muchos aspectos. Cambia la forma de pensar en casi todos los puntos involucrados en el desarrollo de una App Android. Y la navegación no iba a ser menos. ¿Cómo se navega en Jetpack Compose? ¿Qué opciones tenemos?...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de DevExpert, S.L.U., con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *