Muchas apps necesitan mostrar colecciones de elementos. En este documento, se explica cómo puedes hacerlo de manera eficiente en Jetpack Compose.
Si sabes que tu caso de uso no requiere ningún desplazamiento, puede que quieras usar una Column
o una Row
simple (según la dirección) y emitir el contenido de cada elemento iterando en una lista de la siguiente manera:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
Podemos hacer que Column
se pueda desplazar usando el modificador verticalScroll()
.
Listas diferidas
Si necesitas mostrar una gran cantidad de elementos (o una lista de longitud desconocida), el uso de un diseño como Column
puede causar problemas de rendimiento, ya que todos los elementos se compondrán y se dispondrán independientemente de si son visibles o no.
Compose proporciona un conjunto de componentes que solo componen y disponen elementos que están visibles en el viewport del componente. Esos componentes incluyen LazyColumn
y LazyRow
.
Como sugiere el nombre, la diferencia entre LazyColumn
y LazyRow
es la orientación en la que se integran sus elementos y se desplazan. LazyColumn
produce una lista de desplazamiento vertical, mientras que LazyRow
produce una de desplazamiento horizontal.
Los componentes diferidos son distintos en la mayoría de los diseños de Compose. En lugar de aceptar un parámetro de bloque de contenido @Composable
(lo que permite que las apps omitan directamente los objetos componibles), los componentes diferidos proporcionan un bloque LazyListScope.()
. Este bloque LazyListScope
ofrece un DSL que permite que las apps describan el contenido del elemento. Luego, el componente diferido es responsable de agregar el contenido de cada elemento según lo requiera el diseño y la posición de desplazamiento.
DSL de LazyListScope
El DSL de LazyListScope
proporciona una serie de funciones para describir elementos en el diseño. En el caso más básico, item()
agrega un solo elemento, y items(Int)
agrega varios:
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
También hay varias funciones de extensión que te permiten agregar colecciones de elementos, como una List
. Esas extensiones nos permiten migrar fácilmente nuestro ejemplo de Column
anterior:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
También hay una variante de la función de extensión items()
llamada itemsIndexed()
, que proporciona el índice. Consulta la referencia de LazyListScope
para obtener más detalles.
Cuadrículas diferidas
Los elementos que admiten composición LazyVerticalGrid
y LazyHorizontalGrid
admiten la visualización de elementos en una cuadrícula. Una cuadrícula vertical diferida mostrará sus elementos en un contenedor desplazable de forma vertical, que abarcará varias columnas, mientras que las cuadrículas horizontales diferidas tendrán el mismo comportamiento en el eje horizontal.
Las cuadrículas tienen las mismas capacidades de API potentes que las listas y también usan un DSL muy similar (LazyGridScope.()
) para describir el contenido.
El parámetro columns
en LazyVerticalGrid
y el parámetro rows
en LazyHorizontalGrid
controlan el modo en que se forman las celdas en columnas o filas. En el siguiente ejemplo, se muestran elementos de una cuadrícula con GridCells.Adaptive
para que cada columna tenga al menos 128.dp
ancho:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
te permite especificar un ancho para los elementos y, luego, la cuadrícula podrá tener tantas columnas como es posible. Una vez que se calcula la cantidad de columnas, el ancho restante se distribuye de manera equitativa entre las columnas.
Esta forma de tamaño adaptable es especialmente útil para mostrar conjuntos de elementos en diferentes tamaños de pantalla.
Si conoces la cantidad exacta de columnas que se usarán, puedes proporcionar, en cambio, una instancia de GridCells.Fixed
que contenga la cantidad de columnas obligatorias.
Si tu diseño solo requiere que algunos elementos tengan dimensiones no estándar, puedes usar la asistencia de cuadrícula a fin de proporcionar intervalos de columnas personalizados para elementos.
Especifica el intervalo de columnas con el parámetro span
de los métodos LazyGridScope DSL
, item
y items
.
maxLineSpan
, uno de los valores del alcance del intervalo, es muy útil en particular cuando usas el tamaño adaptable, ya que el número de columnas no es fijo.
En este ejemplo, se muestra cómo brindar un intervalo de fila completo:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
Cuadrícula escalonada diferida
LazyVerticalStaggeredGrid
y LazyHorizontalStaggeredGrid
son elementos componibles que te permiten crear una cuadrícula de elementos escalonada y cargada de forma diferida.
Una cuadrícula escalonada vertical diferida muestra sus elementos en un contenedor desplazable vertical que abarca varias columnas y permite que los elementos individuales tengan diferentes alturas. Las cuadrículas horizontales diferidas tienen el mismo comportamiento en el eje horizontal con elementos de diferentes anchos.
El siguiente fragmento es un ejemplo básico del uso de LazyVerticalStaggeredGrid
con un ancho de 200.dp
por elemento:
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(200.dp), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Para establecer una cantidad fija de columnas, puedes usar StaggeredGridCells.Fixed(columns)
en lugar de StaggeredGridCells.Adaptive
.
Esto divide el ancho disponible por la cantidad de columnas (o filas para una cuadrícula horizontal) y hace que cada elemento ocupe ese ancho (o altura para una cuadrícula horizontal):
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(3), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Padding del contenido
En algunas ocasiones, deberás agregar padding alrededor de los bordes del contenido. Los componentes diferidos te permiten pasar algunos PaddingValues
al parámetro contentPadding
para admitir lo siguiente:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
En este ejemplo, agregamos 16.dp
de padding a los bordes horizontales (izquierda y derecha) y, luego, a 8.dp
al principio y al final del contenido.
Ten en cuenta que este padding se aplica al contenido, no a la LazyColumn
. En el ejemplo anterior, el primer elemento agregará un padding de 8.dp
al principio, el último elemento agregará 8.dp
al final, y todos los elementos tendrán un padding de 16.dp
a la izquierda y a la derecha.
Espaciado de contenido
Para agregar espaciado entre elementos, puedes usar Arrangement.spacedBy()
.
En el siguiente ejemplo, se agregan 4.dp
de espacio entre cada elemento:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Del mismo modo que para LazyRow
:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Sin embargo, las cuadrículas aceptan posiciones verticales y horizontales:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
Claves de elementos
De forma predeterminada, el estado de cada elemento se relaciona con la posición del elemento en la lista o cuadrícula. Sin embargo, esto puede causar problemas si cambia el conjunto de datos, ya que los elementos que cambian de posición pierden eficazmente cualquier estado recordado. Imagina el caso de una LazyRow
dentro de una LazyColumn
. Si la fila cambia la posición del elemento, el usuario perderá su posición de desplazamiento dentro de la fila.
Para evitar que esto suceda, puedes proporcionar una clave estable y única para cada elemento, lo que proporciona un bloque al parámetro key
. Cuando se brinda una clave estable, se permite que el estado del elemento sea coherente en los cambios del conjunto de datos:
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
Si proporcionas claves, ayudarás a Compose a controlar correctamente el reordenamiento. Por ejemplo, si tu elemento contiene un estado recordado, la configuración de las claves permitirá a Compose mover este estado junto con el elemento cuando cambia su posición.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
Sin embargo, existe un límite en cuanto a los tipos que puede usar como claves de elemento.
El tipo de clave debe ser compatible con Bundle
, el mecanismo de Android para mantener los estados cuando se vuelve a crear la actividad. Bundle
admite tipos como primitivos, enumeraciones o parcelables.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
La clave debe ser compatible con Bundle
para que se pueda restablecer el rememberSaveable
dentro del elemento que admite composición cuando se vuelva a crear la actividad o incluso cuando te desplaces de este elemento y te desplaces hacia atrás.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
Animaciones de elementos
Si usaste el widget de RecyclerView, sabrás que anima los cambios de los elementos automáticamente.
Los diseños diferidos proporcionan la misma funcionalidad para los reordenamientos de elementos.
La API es simple: solo debes configurar el modificador animateItem
en el contenido del elemento:
LazyColumn { // It is important to provide a key to each item to ensure animateItem() works as expected. items(books, key = { it.id }) { Row(Modifier.animateItem()) { // ... } } }
Incluso puedes proporcionar una especificación de animación personalizada. Para ello, haz lo siguiente:
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItem( fadeInSpec = tween(durationMillis = 250), fadeOutSpec = tween(durationMillis = 100), placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) ) ) { // ... } } }
Asegúrate de brindar claves para tus elementos a fin de que sea posible encontrar la posición nueva del elemento movido.
Encabezados fijos (función experimental)
El patrón "encabezado fijo" es útil para mostrar listas de datos agrupados. A continuación, puedes ver un ejemplo de una "lista de contactos", agrupada por la inicial de cada usuario:
Para lograr un encabezado fijo con LazyColumn
, puedes usar la función experimental stickyHeader()
, que proporciona el contenido del encabezado:
@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
Para obtener una lista con varios encabezados, como el ejemplo de la "lista de contactos" anterior, podrías hacer lo siguiente:
// This ideally would be done in the ViewModel val grouped = contacts.groupBy { it.firstName[0] } @OptIn(ExperimentalFoundationApi::class) @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
Cómo reaccionar a la posición de desplazamiento
Muchas apps deben escuchar cambios en la posición de desplazamiento y el diseño de los elementos, y actuar según corresponda.
Los componentes diferidos admiten este caso de uso elevando el LazyListState
:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
En general, para casos de uso simples, las apps solo necesitan saber información sobre el primer elemento visible. Para esto, LazyListState
proporciona las propiedades firstVisibleItemIndex
y firstVisibleItemScrollOffset
.
Si usamos el ejemplo de mostrar y ocultar un botón según si el usuario se desplazó después del primer elemento:
@Composable fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
Leer el estado directamente en la composición es útil cuando necesitas actualizar otros elementos componibles de la IU, pero también hay situaciones en las que el evento no necesita controlarse en la misma composición. Un ejemplo común de esto es enviar un evento de estadísticas una vez que el usuario se desplaza sobre un punto determinado. Para administrar esto de manera eficiente, podemos usar un snapshotFlow()
:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
LazyListState
también utiliza la propiedad layoutInfo
para proporcionar información sobre todos los elementos que se muestran actualmente y sus límites en la pantalla. Consulta la clase LazyListLayoutInfo
para obtener más información.
Cómo controlar la posición de desplazamiento
Además de reaccionar a la posición de desplazamiento, también es útil que las apps puedan controlarla.
LazyListState
permite esto a través de la función scrollToItem()
, que toma inmediatamente la posición de desplazamiento, y animateScrollToItem()
, que se desplaza con una animación (también conocido como desplazamiento suave):
@Composable fun MessageList(messages: List<Message>) { val listState = rememberLazyListState() // Remember a CoroutineScope to be able to launch val coroutineScope = rememberCoroutineScope() LazyColumn(state = listState) { // ... } ScrollToTopButton( onClick = { coroutineScope.launch { // Animate scroll to the first item listState.animateScrollToItem(index = 0) } } ) }
Conjuntos de datos grandes (Paging)
La biblioteca de Paging permite que las apps admitan listas grandes de elementos, y que se carguen y se muestren pequeños fragmentos de la lista según sea necesario. Paging 3.0 y las versiones posteriores ofrecen Compose a través de la biblioteca de androidx.paging:paging-compose
.
Para mostrar una lista de contenido paginado, podemos usar la función de extensión collectAsLazyPagingItems()
y, luego, pasar los LazyPagingItems
que se muestren a items()
en nuestra LazyColumn
. De manera similar a la compatibilidad con Paging en las vistas, puedes mostrar marcadores de posición mientras se cargan los datos si el item
es null
:
@Composable fun MessageList(pager: Pager<Int, Message>) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it.id } ) { index -> val message = lazyPagingItems[index] if (message != null) { MessageRow(message) } else { MessagePlaceholder() } } } }
Sugerencias para usar diseños diferidos
Hay algunas sugerencias que puedes tener en cuenta para garantizar que tus diseños diferidos funcionen según lo previsto.
Evita usar elementos de 0 píxeles
Esto puede suceder en situaciones en las que, por ejemplo, esperas recuperar asincrónicamente algunos datos como imágenes, para completar los elementos de tu lista en una etapa posterior. Eso provocaría que el diseño diferido componera todos sus elementos en la primera medida, ya que su altura es de 0 píxeles y podría ajustarse a todos en la ventana de visualización. Una vez que se cargaron los elementos y se expandió su altura, los diseños diferidos descartarían todos los demás elementos que se compusieron de forma innecesaria la primera vez, ya que, de hecho, no podrían ajustarse al viewport. Para evitarlo, debes establecer el tamaño predeterminado de tus elementos, de modo que el diseño diferido pueda realizar el cálculo correcto de la cantidad de elementos que pueden caber en el viewport:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
Una vez que conozcas el tamaño aproximado de tus elementos después de que los datos se cargan asincrónicamente, te recomendamos asegurarte de que el tamaño de tus elementos sea el mismo antes y después de la carga, por ejemplo, agregando algunos marcadores de posición. Esta acción ayudará a mantener la posición correcta de desplazamiento.
Evita anidar componentes desplazables en la misma dirección
Este escenario se aplica solo a los casos en los que se anidan los elementos secundarios desplazables sin un tamaño predefinido dentro de otro elemento superior desplazable de la misma dirección. Por ejemplo, anidar un elemento secundario LazyColumn
sin una altura fija dentro de un elemento superior Column
desplazable de forma vertical:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
En su lugar, se puede lograr el mismo resultado si unes todos tus elementos componibles dentro de un LazyColumn
superior y usas el DSL para pasar diferentes tipos de contenido. Como consecuencia, se permite la emisión de elementos únicos, así como varios elementos de lista, todo en un solo lugar:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
Recuerda que se permiten los casos en los que se anidan diferentes diseños de dirección, por ejemplo, un elemento superior desplazable Row
y un elemento secundario LazyColumn
:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
Además de casos en los que aún usas los mismos diseños de dirección, pero también configuraste un tamaño fijo para los elementos secundarios anidados:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
Ten cuidado cuando colocas varios elementos en uno solo
En este ejemplo, la lambda del segundo elemento emite 2 elementos en un bloque:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
Los diseños diferidos se encargarán de esto como se espera: organizarán los elementos uno tras otro como si fueran elementos diferentes. Sin embargo, existen algunos problemas al hacerlo.
Cuando se emiten varios elementos como parte de uno solo, se administran como una entidad, lo que significa que ya no se pueden componer de forma individual. Si un elemento se vuelve visible en la pantalla, todos aquellos correspondientes al elemento deben estar compuestos y medidos. Como consecuencia, se puede ver afectado el rendimiento si se usa de forma excesiva. En el caso extremo de colocar todos los elementos en un elemento, se anula por completo el uso de diseños diferidos. Además de los posibles problemas de rendimiento, si colocas más elementos en otro también interferirá con scrollToItem()
y animateScrollToItem()
.
Sin embargo, existen casos de uso válidos para colocar varios elementos en uno, como tener divisores dentro de una lista. No quieres que los divisores cambien los índices de desplazamiento, ya que no deben considerarse elementos independientes. Además, el rendimiento no se verá afectado, ya que los divisores son pequeños. Es probable que un divisor necesite ser visible cuando el elemento anterior también lo es, por lo que pueden ser parte del elemento anterior:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Considera usar arreglos personalizados
Por lo general, las listas diferidas tienen muchos elementos y ocupan más que el tamaño del contenedor de desplazamiento. Sin embargo, cuando la lista se propaga con pocos elementos, tu diseño puede tener requisitos más específicos sobre el modo en que deberían posicionarse en el viewport.
Para lograr esto, puedes usar la vertical personalizada Arrangement
y pasarla a LazyColumn
. En el siguiente ejemplo, el objeto TopWithFooter
solo necesita implementar el método arrange
. Primero, posicionará los elementos uno tras otro. En segundo lugar, si la altura total que se usa es inferior a la altura del viewport, se posicionará en el pie de página en la parte inferior:
object TopWithFooter : Arrangement.Vertical { override fun Density.arrange( totalSize: Int, sizes: IntArray, outPositions: IntArray ) { var y = 0 sizes.forEachIndexed { index, size -> outPositions[index] = y y += size } if (y < totalSize) { val lastIndex = outPositions.lastIndex outPositions[lastIndex] = totalSize - sizes.last() } } }
Te recomendamos que agregues contentType
.
A partir de Compose 1.2, para maximizar el rendimiento de tu diseño diferido, agrega contentType
a tus listas o cuadrículas. Esto te permite especificar el tipo de contenido de cada elemento del diseño en los casos en que compones una lista o una cuadrícula con varios tipos de elementos:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
Cuando indicas el contentType
, Compose puede reutilizar las composiciones solo entre los elementos del mismo tipo. Como la reutilización es más eficiente cuando compones elementos de estructura similar, proporcionar los tipos de contenido garantiza que Compose no intente componer un elemento de tipo A por encima de un elemento de tipo B completamente diferente. Esto ayuda a maximizar los beneficios de la reutilización de la composición y el rendimiento del diseño diferido.
Mide el rendimiento
Solo puedes medir de manera confiable el rendimiento de un diseño diferido cuando se ejecuta en el modo de lanzamiento y con la optimización de R8 habilitada. En compilaciones de depuración, el desplazamiento diferido del diseño puede parecer más lento. Para obtener más información al respecto, consulta Rendimiento de Compose.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo migrar
RecyclerView
a una lista diferida - Cómo guardar el estado de la IU en Compose
- Kotlin para Jetpack Compose