Esta guía te muestra cómo crear listas en Jetpack Compose con el objetivo de proveer grupos continuos con elementos de texto o imágenes en tus aplicaciones Android. Estas te permitirán presentar colecciones de datos en conjunto de acciones que otorguen valor a tus usuarios.
El contenido está organizado para que aprendas sobre el uso del componente LazyColumn
y la aplicación de los lineamientos en listas para Material Design.
Ejemplo De Listas En Jetpack Compose
Puedes encontrar todo el código completo de los ejemplos de listas en el repositorio Jetpack Compose:
Navega al paquete examples/Lists
del módulo :p7_componentes
y visualiza los archivos Kotlin para cada sección explicada. En ListsScreen.kt
se ubica la función componible de la pantalla que condensa todos los ejemplos:
1. Componentes Para Crear Listas
1.1 Lista Pequeña Con Column
Usa los layouts Column
o Row
para crear listas pequeñas. Es decir, colecciones con una cantidad fija de ítems, cuyo contenido se mostrará por completo y no requerirá scroll (o solo para unos pocos).
La forma de hacerlo consiste en:
- Crear la función componible que representa a la lista (
Column
para verticales,Row
para horizontales) - (Opcional) Aplicar un modificador
verticalScroll()
uhorizontalScroll()
si la lista requiere scroll - Crear la función componible para el diseño del ítem
- Definir un bucle sobre la colección (normalmente la función
forEach()
) - Invocar la función del ítem al interior del bucle
Evidenciemos estas tareas en la función de ejemplo ListWithColumn()
:
@Composable
fun ListWithColumn(items: List<String>) { // (1)
val scrollState = rememberScrollState()
Column(modifier = Modifier.verticalScroll(scrollState)) { // (2)
items.forEach { item -> // (4)
ListItemRow(item) // (5)
}
}
}
@Composable // (3)
fun ListItemRow(item: String, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.height(48.dp)
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.CenterStart
) {
Text(text = item, style = MaterialTheme.typography.subtitle1)
}
}
El resultado es:
Si incrementas el número de ítems a proyectar, comenzarás a notar la introducción de un congelamiento de UI. Esto se debe a que Column
procesa todos los elementos en cada recomposición, incluso si no se ven. Por lo que a mayor número, menos fluidez.
Justo por esta motivación, necesitamos el siguiente componente.
1.2 Lista Grande Con LazyColumn
Usa las funciones LazyColumn()
y LazyRow()
para crear una lista desplazable que solo compone y dibuja los elementos visibles en un espacio determinado. Dicho comportamiento nos permite presentar colecciones de gran tamaño sin preocuparnos por el rendimiento.
Nota: El componente
LazyColumn
equivale alListView
en JetpackCompose (oRecyclerView
) del sistema de views.
Su definición es la siguiente:
@Composable
fun LazyColumn(
modifier: Modifier! = Modifier,
state: LazyListState! = rememberLazyListState(),
contentPadding: PaddingValues! = PaddingValues(0.dp),
reverseLayout: Boolean! = false,
verticalArrangement: Arrangement.Vertical! = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal! = Alignment.Start,
flingBehavior: FlingBehavior! = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean! = true,
content: (@ExtensionFunctionType LazyListScope.() -> Unit)?
): Unit
Donde:
state
: Representa el estado de la lista, en específico datos sobre el primer ítem visible y estado de scrollingcontentPadding
: Cantidad de relleno agregado al contenido de toda la listareverseLayout
: Determina si se debe invertir la dirección del scroll y el orden en que se muestran los ítemsverticalArrangement
: Determina la disposición vertical de los ítems. De gran utilidad para añadir espacio vertical entre elementos y para distribuirlos cuando hay un mínimo de altura establecidohorizontalAlignment
: Alineación horizontal aplicada a los ítemsflingBehavior
: Especifica la animación que es ejecutada cuando se termina un evento de arrastre en la listauserScrollEnabled
: Determina si el scrolling por eventos del usuario o acciones de accesibilidad está activocontent
: Una lambda con recibidor de tipoLazyListScope
, con un cuerpo que determinará el contenido de la lista. Claramente este representa un DSL que otorga legibilidad y nos exime de integrar los ítems en el layout.
Ahora bien, la siguiente lista de pasos te permite crear el contenido de una lista vertical con LazyColumn
:
- Crea la función con un componente
LazyColumn
y eleva los estados necesarios - Crea la diseño del ítem
- Crea los ítems con las funciones
item()
oitems()
deLazyListScope
Con esta receta en mente podemos adaptar el ejemplo anterior con la nueva función ListWithLazyColumn()
:
@Composable
fun ListWithLazyColumn(items: List<String>) { // (1)
LazyColumn {
items(items) { item ->
ListItemRow(item)
}
}
}
// (2) El mismo diseño anterior
fun ListItemRow(item: String, modifier: Modifier = Modifier) {
...
}
Cómo ves, items()
recibe una lista de elementos que serán desplegados como funciones componibles sobre la lista. En el caso de que desees poner ítems individualmente, usa item()
.
2. Propiedades De LazyColumn
Veamos ejemplos de efectos causados al modificar algunos parámetros estéticos de LazyColumn
.
2.1 Cambiar Padding Del Contenido
contentPadding
Cuando desees aumentar el padding general de la lista, pasa otra instancia PaddingValues
que determine el valor en cada lado.
Normalmente lo usamos para dar 8dp en la parte superior e inferior de la lista:
LazyColumn(
contentPadding = PaddingValues(vertical = 8.dp),
) {
//...
}
El siguiente es un ejemplo donde se usa un Slider
para aplicar valores de 0 a 100 sobre contentPadding
:
@OptIn(ExperimentalUnitApi::class)
@Composable
fun ListWithContentPadding() {
val items = List(10) { "Item ${it + 1}" }
Column {
val range = 0f..100f
var selection by remember { mutableStateOf(0f) }
Text(
text = "contentPadding = PaddingValues(all = ${selection.toInt()}.dp)",
fontFamily = FontFamily.Monospace,
fontSize = TextUnit(12f, TextUnitType.Sp),
modifier = Modifier
.padding(16.dp)
.align(CenterHorizontally)
)
Slider(
value = selection,
valueRange = range,
onValueChange = {
selection = it
},
modifier = Modifier.padding(horizontal = 16.dp)
)
Divider(modifier = Modifier.padding(vertical = 16.dp))
LazyColumn(
contentPadding = PaddingValues(all = selection.dp), // Padding
modifier = Modifier.background(color = Color(0xFFE0F7FA))
) {
items(items) { item ->
ListItemRow(item, modifier = Modifier.background(Color.White))
}
}
}
}
Como ves, el estado selection
es usado como padding de todos los lados al usar el parámetro nombrado all
en el constructor de PaddingValues
.
2.2 Cambiar Espaciado Del Contenido
verticalArrangement
Como viste en la firma de LazyColumn
, el parámetro verticalArragement
(horizontalArragement
en LazyRow
) aplica espaciado entre los ítems. Para producir dicho valor usa Arrangement.spacedBy()
.
Por ejemplo, añadamos 16dp de espaciado entre los ítems:
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
//...
}
De manera similar al ejemplo anterior, en el repositorio encontrarás la función ListWithContentSpacing()
para evidenciar el cambio de espaciado a partir de un Slider
.
@Composable
fun ListWithContentSpacing() {
val items = List(10) { "Item ${it + 1}" }
Column {
val range = 0f..100f
var contentSpacing by remember { mutableStateOf(0f) }
CodeText(
text = "verticalArrangement = spacedBy(${contentSpacing.toInt()}.dp)",
modifier = Modifier.align(CenterHorizontally)
)
Slider(
value = contentSpacing,
valueRange = range,
onValueChange = {
contentSpacing = it
},
modifier = Modifier.padding(horizontal = 16.dp)
)
Divider(modifier = Modifier.padding(vertical = 16.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(contentSpacing.dp),
modifier = Modifier.background(color = Color(0xFFE0F7FA))
) {
items(items) { item ->
ListItemRow(item, modifier = Modifier.background(Color.White))
}
}
}
}
2.3 Añadir Divisores
Usa al componente Divider
dentro de item()
o Items()
para mostrar un divisor entre los ítems de la lista:
@Composable
fun ListWithDividers() {
val items = List(10) { "Item ${it + 1}" }
Column {
LazyColumn {
items(items) { item ->
ListItemRow(item)
Divider() // Divisor por debajo
}
}
}
}
2.4 Añadir Cabeceras Fijas
stickyHeader
)Si deseas agrupar por algún criterio a los ítems de tu lista, el uso de cabeceras fijas te vendrá excelente. Estos rótulos se ubican arriba del conjunto de ítems y se mantienen en el extremo superior de la lista al scrollear.
Créalas con la función stickyHeader()
al interior del contenido, pasando como parámetro la función componible que representa su diseño.
Por ejemplo: Tomemos una lista y dividamosla en grupos de cinco elementos :
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithStickyHeaders() {
val items = List(30) { "Item ${it + 1}" }
val groups = items.chunked(5) // División
LazyColumn {
groups.forEachIndexed { index, group ->
stickyHeader {
Header("Grupo ${index + 1}")
}
items(group) { item ->
ListItemRow(item)
}
}
}
}
@Composable // Diseño de cabecera
private fun Header(title: String) {
Surface(
color = Color(0xFFE1F5FE),
modifier = Modifier.fillMaxWidth()
) {
Text(text = title, Modifier.padding(horizontal = 16.dp, vertical = 4.dp))
}
}
La función chunked()
es de gran utilidad para dividir en grupos de cinco y así iterar para la creación de las cabeceras.
Si agrupas por atributos, entonces usa groupBy()
o similares.
2.5 Añadir Lista Horizontal Dentro De Lista Vertical
Mostrar una lista horizontal al interior de una vertical solo es cuestión de la invocación de Row
o LazyRow
en un elemento item()
del contenido.
Por ejemplo: Supongamos que mostraremos diez items recomendados como primer ítem de la lista vertical. Estos tendrán un diseño con una imagen superior y una descripción inferior (ver figura anterior).
Comencemos diseñando el layout de los items horizontales:
@Composable
private fun HorizontalItem(text: String) {
Column(
modifier = Modifier
.border(
width = 1.dp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
shape = RoundedCornerShape(topStartPercent = 20)
)
.width(120.dp),
horizontalAlignment = CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.ic_image),
contentDescription = null,
modifier = Modifier.size(120.dp),
contentScale = ContentScale.Crop
)
Box(
Modifier
.fillMaxWidth()
.background(Color.LightGray),
contentAlignment = Center
) {
Text(
text = text,
modifier = Modifier.padding(vertical = 8.dp)
)
}
}
}
Seguido, diseñemos el layout de la lista horizontal como item hijo de la vertical:
@Composable
private fun FirstItem(recommended: List<String>) {
Column {
Text(text = "Item 1", modifier = Modifier.padding(start = 16.dp, bottom = 8.dp))
LazyRow(
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(
items = recommended,
key = { itemAsId -> itemAsId }
) { item ->
HorizontalItem(item)
}
}
}
}
Claves De Ítems: Como notas, se incluyó el uso del atributo
key
en la funciónitem()
. Esto permite recordar la posición del scroll por si los ítems llegan a cambiar de orden.
Y finalmente, dentro del LazyListScope combinemos la lista horizontal con los demás ítems:
@Composable
fun VerticalListWithHorizontalList() {
val items = List(15) { "Item ${it + 2}" }
val itemsForFirstRow = List(10) { "Item ${it + 1}" }
LazyColumn {
item {
FirstItem(itemsForFirstRow) // Lista horizontal
}
items(items) { item -> // Los demás ítems
ListItemRow(item)
}
}
}
Al ejecutar, verás que al scrollear ambas listas no habrá ningún problema:
3. Gestos Sobre Listas
3.1 Click En Item De Lista
Reaccionar ante el clic de un ítem de la lista es posible al aplicar el modificador clickable()
al componente LazyColumn
/LazyRow
.
Ejemplo: Comprobemos el funcionamiento mostrando un Toast
con el nombre del item clickeado.
La solución:
@Composable
fun ClickableListItems() {
val items = List(5) { "Item ${it + 1}" }
val context = LocalContext.current
LazyColumn {
items(items) { item ->
ListItemRow(
item = item,
modifier = Modifier.clickable {
Toast.makeText(context, "Clic en '$item'", Toast.LENGTH_SHORT).show()
}
)
}
}
}
3.2 Eventos De Scrolling
Para responder a los cambios de scroll en tu lista usa, crea un estado para recordarlos con rememberLazyListState()
y luego pásalo al parámetro state
de LazyColumn
.
El estado producido es del tipo LazyListState
, cuyo constructor recibe las siguientes propiedades:
LazyListState(
firstVisibleItemIndex: Int!,
firstVisibleItemScrollOffset: Int!
)
firstVisibleItemIndex
hace referencia al índice del primer ítem visible en el viewport y firstVisibleItemScrollOffset
es la cantidad de desplazamiento actual desde dicho ítem. Ambas son propiedades observables, por lo que es necesario usar efecto secundarios (todo) al encadenar cambios de estado.
Por ejemplo: Mostremos en pantalla los valores de las dos propiedades de scrolling al interactuar con la lista.
@Composable
fun ScrollingEvents() {
val items = List(20) { "Item $it" }
val scrollState = rememberLazyListState()
val scrollSummary = "firstVisibleItemIndex {${scrollState.firstVisibleItemIndex}}, " +
"firstVisibleItemScrollOffset {${scrollState.firstVisibleItemScrollOffset}}"
Column {
CodeText(text = scrollSummary)
Divider(modifier = Modifier.padding(vertical = 16.dp))
LazyColumn(state = scrollState) {
items(items) { item ->
ListItemRow(item)
}
}
}
}
3.3 Scrollear Hacia Un Ítem
Si deseas scrollear una lista programáticamente usa los métodos scrollToItem()
y animateScrollToItem()
de LazyListState
.
¿La diferencia entre ambos?
El segundo realiza una animación para que el scroll se vea fluido, mientras que el segundo desplaza inmediatamente el contenido.
Por ejemplo: Usemos un TextField
para recibir la posición a la que deseamos scrollear en una lista vertical.
La solución consiste de la toma del estado del campo de texto y luego aplicarlo con animateScrollToItem()
:
@Composable
fun ProgrammaticScrolling() {
val items = List(30) { "Item ${it + 1}" }
val scrollState = rememberLazyListState()
var itemPosition by remember { mutableStateOf(0) }
val coroutineScope = rememberCoroutineScope() // (1)
Column {
TextField(
value = if (itemPosition == 0) "" else itemPosition.toString(),
onValueChange = { value ->
itemPosition = (value.toIntOrNull() ?: 0)
coroutineScope.launch { // (2)
scrollState.animateScrollToItem((itemPosition - 1).coerceIn(0..items.size))
}
},
label = { Text(text = "Posición del ítem") },
modifier = Modifier.padding(horizontal = 16.dp)
)
Divider(modifier = Modifier.padding(vertical = 16.dp))
LazyColumn(state = scrollState) {
items(items) { item ->
ListItemRow(item)
}
}
}
}
Del código anterior hay dos cosas que destacar. La primera, hemos definido un alcance de corrutina (rememberCoroutineScope()
) para ejecutar el scrolling programático.
Y la segunda, usamos coerceIn()
para forzar las entradas de texto entre 0 y 30, con el fin de evitar errores de conversión de String
a Int
.
4. Controles De Lista
En ciertos escenarios, la lista requerirá añadir acciones para los items en el espacio del slot secundario. Veamos algunos de ellos:
4.1 Lista Con CheckBox
Incluiremos un componente CheckBox
en el ítem cuando deseemos crear una lista seleccionable. Esto es, aplicar acciones sobre los elementos marcados.
Por ejemplo: Contemos los items seleccionados de una lista de una línea.
En primer lugar, crearemos un item de lista que reciba un control en la parte derecha de su diseño:
@Composable
fun ListItemRowWithControl(
item: String,
control: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxWidth()
.height(48.dp)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = item,
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.align(
Alignment.CenterStart
)
)
Box(modifier = Modifier.align(Alignment.CenterEnd)) {
control()
}
}
}
Luego, creamos la función para la lista seleccionable, donde:
- Declaramos un estado para almacenar los items seleccionados
- Creamos un texto para mostrar la cantidad de ítems
- Creamos una
LazyColumn
con itemsSelectableRow
- El componente
SelectableRow
usa aListItemRowWithControl
para introducir elCheckBox
y dibujar el estado de marcado - Incrementamos/decrementamos el registro de selecciones cuando cambie el estado del CheckBox
En código es:
@Composable
fun SelectableList() {
val items = List(5) { "Item ${it + 1}" }
val selections = remember { mutableStateListOf<String>() } // (1)
Column {
Text(text = "Items seleccionados: ${selections.size}", modifier = Modifier.padding(16.dp)) // (2)
LazyColumn { // (3)
items(items) { item ->
SelectableRow(
text = item,
onCheckedChange = { isChecked ->
if (isChecked) { // (5)
selections += item
} else {
selections -= item
}
}
)
}
}
}
}
@Composable // (4)
private fun SelectableRow(
text: String,
onCheckedChange: (Boolean) -> Unit
) {
var checked by remember { mutableStateOf(false) }
ListItemRowWithControl(
item = text,
control = {
Checkbox(
checked = checked,
onCheckedChange = { value ->
checked = value
onCheckedChange(value)
}
)
},
modifier = Modifier
)
}
4.2 Lista Con Switch
Switch
El componente Switch
es agregado mayormente en la pantalla de ajustes, donde encontramos preferencias dependientes del estado activo de otra.
¿Cómo se comporta?
Al activar el switch se despliegan items de la lista asociados a la preferencia particular.
Ejemplo: Supongamos que existe una característica que al estar activa permite configurar dos preferencias asociadas.
Para conseguir dicha relación, añadimos un elemento Switch
al ítem definido anteriormente. Cada que su estado cambie, modificaremos su tamaño para mostrar u ocultar los subitems asociados.
Veamos:
@Composable
fun SwitchPreferenceItem() {
var checked by remember { mutableStateOf(false) }
ListItemRowWithControl(
item = "Preferencia",
control = {
Switch(checked = checked, onCheckedChange = { checked = it })
},
modifier = Modifier
)
val modifier = if (checked) Modifier.wrapContentHeight() else Modifier.height(0.dp)
Column(modifier = modifier.animateContentSize()) {
ListItemRow("Dependencia 1")
ListItemRow("Dependencia 2")
}
}
animateContentSize()
nos ayuda a mostrar una animación entre el cambio de altura del ítem con detalles internos.
Nota: Si quieres un mejor control sobre la animación, puedes usar
AnimatedVisibility
4.3 Icono Para Expandir Y Colapsar
Crear una lista expandible es similar a lo que hicimos con la acción secundaria que tiene un Switch
, solo que aplicado uniformemente sobre todos los ítems de primer nivel de la lista.
Llevarlo a cabo requiere de:
- Crear una clase de datos con la definición del ítem con sus detalles a expandir
- Crear el componente para las filas expandibles
- Ubicar un icono con forma de flecha hacia abajo en estado de expansión y hacia arriba en estado de contracción
- Crear la lista expandible
Observa la implementación:
// (1)
data class ExpandableItem(val item: String, val details: List<String>)
// (4)
@Composable
fun ExpandableList() {
val items = List(5) { item ->
ExpandableItem(
item = "Item ${item + 1}",
details = List(3) { "Detalle ${item + 1}.${it + 1}" }
)
}
LazyColumn {
items(items) { item ->
ExpandableRow(item)
}
}
}
// (2)
@Composable
private fun ExpandableRow(expandableItem: ExpandableItem) {
var isExpanded by remember { mutableStateOf(false) }
if (isExpanded)
Divider()
ListItemRowWithControl(
item = expandableItem.item,
control = {
ExpandCollapseIcon( // (3)
expanded = isExpanded,
onIconClick = { isExpanded = !isExpanded })
}
)
val modifier = if (isExpanded) Modifier.wrapContentHeight() else Modifier.height(0.dp)
Column(modifier = modifier.animateContentSize()) {
expandableItem.details.forEach {
ListItemRow(it)
}
}
if (isExpanded)
Divider()
}
// (3)
@Composable
private fun ExpandCollapseIcon(
expanded: Boolean,
onIconClick: () -> Unit = {}
) {
IconButton(onClick = onIconClick) {
Icon(
Icons.Filled.ArrowDropDown,
"Icono de control para lista expandible",
Modifier.rotate(
if (expanded)
180f
else
360f
)
)
}
}
5. Cambiar El Tema De Una Lista
Al igual que los demás componentes de Compose, es posible temificar una lista usando modificadores a nivel individual en LazyColumn
y sus ítems.
O también puedes crear una instancia de MatherialTheme
, copiar el tema actual y modificar colores, tipografía y formas como desees.
Ejemplo: Se requiere una lista con un color de superficie Índigo 500 y que sus ítems tengan un fondo Indigo 900 con letras de color blanco; y una forma redondeada al 50% (con en la figura anterior).
Para satisfacer los requisitos previos, configuramos el tema de esta forma:
@Composable
fun CustomList() {
val items = List(5) { "Item ${it + 1}" }
val indigo500 = Color(0xFF3F51B5)
val indigo900 = Color(0xFF1A237E)
MaterialTheme(
colors = MaterialTheme.colors.copy(surface = indigo500, onSurface = Color.White),
) {
Surface(modifier = Modifier.fillMaxSize()) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(items) { item ->
ListItemRow(
item = item,
modifier = Modifier.background(
color = indigo900,
shape = RoundedCornerShape(50)
)
)
}
}
}
}
}