LazyColumn — qué es, listas eficientes en Compose

Autor: IT Sectr Publicado: 2026-02-24 Tiempo de lectura: 10 min

LazyColumn — un componente de Jetpack Compose para mostrar listas desplazables con carga perezosa. A diferencia de RecyclerView, LazyColumn no crea ViewHolders ni usa XML — todos los elementos se describen declarativamente mediante funciones composables. Te mostramos qué es LazyColumn, cómo funcionan LazyListState y el API items, y cómo lograr un rendimiento de 60 FPS. En IT Sectr usamos LazyColumn en todos los proyectos nuevos de Compose. Para comparar con el enfoque clásico, lee el artículo sobre RecyclerView.

Puntos clave

  • LazyColumn — un componente de Jetpack Compose para listas verticales con carga perezosa, parte de Material Design 3.
  • LazyListState — estado de la lista: posición de scroll, elementos visibles, desplazamiento programático.
  • items API — funciones items(), itemsIndexed(), items() con claves para vinculación de datos.
  • key — parámetro para identificación estable de elementos, permitiendo a Compose reutilizar la composición.
  • LazyVerticalGrid — versión de LazyColumn para cuadrículas con número fijo de columnas.

¿Qué es LazyColumn en Jetpack Compose?

LazyColumn es una función composable de la biblioteca Foundation Compose que muestra una lista vertical de elementos con carga perezosa: solo se crean y componen los elementos visibles en pantalla o cercanos (zona de prefetch). LazyColumn apareció en Compose 1.0 (2021) junto con LazyRow (lista horizontal) y LazyVerticalGrid (cuadrícula).

La carga perezosa funciona mediante el mecanismo SubcomposeLayout: LazyColumn mide el espacio disponible, solicita el rango visible a LayoutInfo y compone solo los elementos en ese rango. Los elementos que salen de pantalla abandonan la composición (excepto el búfer de prefetch). Según Google (Android Performance, 2026), LazyColumn mantiene 60 FPS al desplazarse por una lista de 10000+ elementos en dispositivos de gama media.

Diferencia con Column + ScrollState

Column con scroll vertical es una disposición normal de todos los elementos uno debajo de otro con el modificador verticalScroll aplicado. Column no es perezosa: todos los elementos hijos se componen inmediatamente, incluso si no son visibles. Para una lista de 200+ elementos, Column causa un lag significativo al iniciar. LazyColumn compone solo los elementos visibles + búfer (1 pantalla adelante/atrás por defecto), lo que la hace indispensable para listas largas.

items API: vinculación de datos a la lista

items API es un conjunto de funciones de extensión para LazyColumn que aceptan una lista de datos y un descriptor DSL para cada elemento. Las funciones principales son: items(count, key, itemContent) para cantidad fija, items(list, key, itemContent) para una lista, itemsIndexed(list, key, itemContent) con índice. El parámetro key es obligatorio para el rendimiento — le da a Compose un identificador estable de elemento.

kotlin
// LazyColumn básica con itemContent
@Composable
fun ArticleList(articles: List<Article>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header — no perezoso, siempre al inicio
        item {
            Text("Últimos artículos", style = MaterialTheme.typography.headlineMedium)
        }

        // Lista de artículos con clave por id
        items(articles, key = { it.id }) { article ->
            ArticleCard(
                title = article.title,
                summary = article.summary,
                onClick = { onArticleClick(article.id) }
            )
        }

        // Footer con indicador de carga
        item {
            CircularProgressIndicator(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            )
        }
    }
}

// Elemento de lista personalizado
@Composable
fun ArticleCard(title: String, summary: String, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
    ) {
        Column(modifier = Modifier.padding(12.dp)) {
            Text(text = title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = summary, style = MaterialTheme.typography.bodyMedium)
        }
    }
}

item {} vs items(): usa item {} para elementos individuales que siempre están presentes en la lista (header, footer, divisores). Usa items() para una lista dinámica de datos. items() acepta un Iterable o Int y un bloque DSL que se llama para cada elemento. Combina item e items en un mismo LazyColumn: el orden de las llamadas determina el orden de visualización.

LazyListState: gestión de la posición de scroll

LazyListState es un objeto que almacena el estado de la lista: posición actual de scroll (firstVisibleItemIndex, firstVisibleItemScrollOffset), elementos visibles (layoutInfo) y métodos de desplazamiento programático (scrollToItem, animateScrollToItem). LazyListState se crea mediante rememberLazyListState() y se pasa a LazyColumn a través del parámetro state.

kotlin
// LazyColumn con preservación de posición de scroll
@Composable
fun ScrollingList(items: List<Item>) {
    val listState = rememberLazyListState()

    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn(state = listState) {
            items(items, key = { it.id }) { item ->
                ListItemView(item = item)
            }
        }

        // Botón "Subir" aparece después del 10.º elemento
        val showButton by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 10 }
        }

        AnimatedVisibility(visible = showButton) {
            FloatingActionButton(
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(16.dp),
                onClick = {
                    coroutineScope.launch {
                        listState.animateScrollToItem(0)
                    }
                }
            ) {
                Icon(Icons.Default.KeyboardArrowUp, "Subir")
            }
        }
    }
}

// Obtención de información de elementos visibles
@Composable
fun ListDebugInfo(listState: LazyListState) {
    val visibleItems = listState.layoutInfo.visibleItemsInfo
    val totalItems = listState.layoutInfo.totalItemsCount
    val firstItem = visibleItems.firstOrNull()?.index
    val lastItem = visibleItems.lastOrNull()?.index

    Text("Mostrando $firstItem–$lastItem de $totalItems")
}

El desplazamiento programático se realiza mediante funciones suspend de listState: scrollToItem(index, scrollOffset) — movimiento instantáneo (sin animación), animateScrollToItem(index) — con animación. Para desplazamiento desde una corrutina, usa LaunchedEffect o coroutineScope.launch. Importante: no llames funciones suspend desde la composición (dentro de @Composable) — usa lambdas (onClick, LaunchedEffect).

LazyColumn vs RecyclerView: comparación de enfoques

LazyColumn y RecyclerView resuelven el mismo problema — visualización eficiente de listas largas. La diferencia está en la arquitectura: RecyclerView usa ViewHolder y Adapter (vista clásica de Android), LazyColumn usa funciones composables declarativas sin XML ni ViewHolder. La elección depende del stack tecnológico del proyecto: Compose vs UI basada en View.

ParámetroLazyColumn (Compose)RecyclerView (View)
DiseñoFunciones composables (Kotlin DSL)Archivos XML + ViewBinding
Adaptadoritems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
Ordenamiento/FiltradoSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
AnimaciónAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
Tipos de elementositem {}/items() + when por tipogetItemViewType + varios ViewHolders
Scroll a posiciónlistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchEn proceso (búfer SubcomposeLayout)RecycledViewPool + GapWorker
Red/PaginacióncollectAsLazyPagingItems() + Paging 3PagingDataAdapter + Paging 3

Recomendación de Google (Android Developers, 2026): para proyectos nuevos en Jetpack Compose, usa LazyColumn. Para proyectos basados en View, usa RecyclerView. LazyColumn requiere Compose, que añade unos 3–5 MB al APK — tenlo en cuenta al soportar dispositivos antiguos. Si un proyecto ya usa el sistema View, LazyColumn no puede coexistir en una misma pantalla con diseño XML sin AndroidView — en tales casos, es más fácil quedarse con RecyclerView.

LazyVerticalGrid y LazyHorizontalGrid: cuadrículas

LazyVerticalGrid es una versión de LazyColumn para cuadrículas con número fijo de columnas. LazyHorizontalGrid es una cuadrícula horizontal con filas fijas. Ambos componentes usan los mismos principios de carga perezosa y el mismo API items que LazyColumn. El parámetro columns determina el número de columnas mediante GridCells.Fixed(N) o GridCells.Adaptive(minSize).

kotlin
// Cuadrícula con columnas adaptativas
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 128.dp),
        contentPadding = PaddingValues(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(photos, key = { it.id }) { photo ->
            AsyncImage(
                model = photo.url,
                contentDescription = photo.title,
                modifier = Modifier.aspectRatio(1f)
            )
        }
    }
}

// Lista horizontal (LazyRow)
@Composable
fun CategoryCarousel(categories: List<Category>) {
    LazyRow(
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(categories, key = { it.id }) { category ->
            FilterChip(
                selected = category.isSelected,
                onClick = { onCategoryClick(category.id) },
                label = { Text(category.name) }
            )
        }
    }
}

GridCells.Adaptive calcula automáticamente el número de columnas basándose en el tamaño mínimo de celda. Por ejemplo, GridCells.Adaptive(128.dp) en una pantalla de 360 dp de ancho coloca 2 columnas, en una tableta de 600 dp — 4 columnas. GridCells.Fixed(2) siempre muestra exactamente 2 columnas. Adaptive es preferible para adaptación a múltiples pantallas — se ajusta automáticamente al ancho de pantalla sin consultas de medios.

Rendimiento de LazyColumn: key, contentType, prefetch

El rendimiento de LazyColumn depende de tres factores: estabilidad de clave (key), tipo de contenido (contentType) y búfer de prefetch (beyondBounds). Por defecto, LazyColumn almacena en búfer 1 pantalla adelante y 1 atrás. Google (Android Developers, 2026) recomienda configurar estos parámetros para listas con diferentes tipos de elementos o composición compleja.

ParámetroDescripciónRecomendación
keyIdentificador estable de elementoEspecifica siempre key = { it.id }. Sin key, Compose usa el índice, y el reordenamiento de elementos rompe la animación.
contentTypeTipo de contenido para separación de gruposEspecifica para listas con diferentes tipos de elementos (texto + imagen + anuncio). Compose reutilizará la composición solo dentro del mismo contentType.
beyondBoundsNúmero de pantallas de prefetchAumenta a 2–3 para listas con imágenes pesadas. Por defecto es 1 pantalla.
Modifier.animateItemAnimación de cambio de posiciónÚsalo para ordenamiento y filtrado animados (Compose 1.7+).
kotlin
// LazyColumn optimizada con contentType y prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        modifier = Modifier.fillMaxSize(),
        beyondBoundsPageCount = 2  // prefetch 2 pantallas
    ) {
        items(
            items = items,
            key = { it.id },
            contentType = { it.type }  // división por tipo
        ) { item ->
            when (item) {
                is FeedItem.Post -> PostView(item)
                is FeedItem.Ad -> AdView(item)
                is FeedItem.Suggested -> SuggestedView(item)
            }
        }
    }
}

// Medición de rendimiento mediante listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
    val scrollInfo = listState.layoutInfo
    val visibleCount = scrollInfo.visibleItemsInfo.size
    val total = scrollInfo.totalItemsCount

    // derivedStateOf — no se recompone si el valor no ha cambiado
    val scrollProgress by remember {
        derivedStateOf {
            if (total > 0) visibleCount.toFloat() / total else 0f
        }
    }

    LinearProgressIndicator(
        progress = scrollProgress,
        modifier = Modifier.fillMaxWidth().height(2.dp)
    )
}

Errores comunes: (1) falta de key — los elementos se recomponen ante cualquier cambio en la lista; (2) usar mutableStateListOf sin flujos snapshot — los cambios pueden no rastrearse; (3) llamar funciones suspend dentro de itemContent — interrumpe la composición; (4) sin contentType para tipos mixtos — Compose reutiliza la composición de anuncios para publicaciones de texto, causando artefactos visuales. En IT Sectr añadimos contentType a todas las listas con tres o más tipos de contenido.

Preguntas frecuentes

LazyColumn se desplaza lentamente — ¿qué hacer?

Revisa tres parámetros: (1) key — sin una clave estable, Compose no puede reutilizar la composición, (2) contentType — para listas con diferentes tipos de elementos, (3) beyondBoundsPageCount — aumenta a 2 para imágenes. Mueve los cálculos pesados dentro de itemContent a remember con una clave por datos. Usa Modifier.drawWithContent en lugar de Image para gráficos simples.

¿Cómo añadir un divisor entre elementos de LazyColumn?

Usa LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) para espaciado entre elementos. Para un divisor visual con línea, añade item { Divider() } entre los items. Divisor automático: items(items, key = { it.id }) { item -> ... }, y entre cada elemento inserta item { HorizontalDivider() }. Para esto, usa LazyListScope.items(list, key) { /* elemento */ } y por separado LazyListScope.item { Divider() }.

LazyColumn dentro de Column no funciona — ¿por qué?

LazyColumn tiene altura infinita (fillMaxSize) por defecto. Dentro de una Column sin altura fija, LazyColumn no puede determinar su tamaño. Solución: (1) asigna a LazyColumn una altura fija: Modifier.height(400.dp), (2) usa Modifier.weight(1f) dentro de Column, (3) no anides LazyColumn dentro de un contenedor con scroll vertical — usa LazyColumn como elemento raíz con item {} para header y footer.

¿Cómo actualizar LazyColumn al cambiar los datos?

LazyColumn reacciona automáticamente a los cambios de State. Si los datos se almacenan en mutableStateListOf o mutableStateOf, Compose recompone solo los elementos cambiados. Para listas desde ViewModel, usa collectAsState() con Flow. Al actualizar la lista con key, Compose anima automáticamente los cambios (adición, eliminación, reordenamiento) mediante Modifier.animateItemPlacement().

LazyColumn vs LazyVerticalGrid — ¿cuál elegir?

LazyColumn — para listas de una columna (feed, chat, comentarios). LazyVerticalGrid — para cuadrículas (galería, catálogo, iconos). Si los elementos deben estar en una columna en teléfono y en dos en tableta — usa LazyVerticalGrid con GridCells.Adaptive. Si se necesita una cuadrícula mixta (diferente número de columnas en una sección) — usa LazyColumn e inserta LazyRow horizontal o LazyVerticalGrid anidada dentro de los items.

Resumen

  • LazyColumn — un componente de Jetpack Compose para listas verticales perezosas con API declarativo.
  • items() API acepta una lista y un bloque DSL para cada elemento; key es obligatorio para el rendimiento.
  • LazyListState gestiona la posición de scroll, proporciona funciones suspend para desplazamiento programático.
  • contentType separa los grupos de composición para listas con diferentes tipos de elementos.
  • LazyVerticalGrid / LazyRow — variantes para cuadrículas y listas horizontales con el mismo API items.
  • LazyColumn mantiene 60 FPS para 10000+ elementos gracias a SubcomposeLayout y al búfer de prefetch.
  • Para proyectos nuevos en Compose usa LazyColumn; para proyectos basados en View — RecyclerView.

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también