LazyColumn — co to jest, efektywne listy w Compose

Autor: IT Sectr Opublikowano: 2026-02-24 Czas czytania: 10 min

LazyColumn — komponent Jetpack Compose do wyświetlania przewijanych list z leniwym ładowaniem. W przeciwieństwie do RecyclerView, LazyColumn nie tworzy ViewHolderów i nie używa XML — wszystkie elementy opisane są deklaratywnie przez funkcje composable. Pokazujemy, czym jest LazyColumn, jak działa LazyListState i items API, oraz jak osiągnąć wydajność na poziomie 60 FPS. W IT Sectr używamy LazyColumn we wszystkich nowych projektach Compose. Dla porównania z klasycznym podejściem przeczytaj artykuł o RecyclerView.

Najważniejsze

  • LazyColumn — komponent Jetpack Compose do list pionowych z leniwym ładowaniem, część Material Design 3.
  • LazyListState — stan listy: pozycja przewijania, widoczne elementy, programowe przewijanie.
  • items API — funkcje items(), itemsIndexed(), items() z kluczami do wiązania danych.
  • key — parametr do stabilnej identyfikacji elementów, pozwalający Compose na ponowne użycie kompozycji.
  • LazyVerticalGrid — wersja LazyColumn dla siatek o stałej liczbie kolumn.

Czym jest LazyColumn w Jetpack Compose?

LazyColumn — to funkcja composable z biblioteki Foundation Compose, która wyświetla pionową listę elementów z leniwym (lazy) ładowaniem: tworzone i komponowane są tylko te elementy, które są widoczne na ekranie lub znajdują się w pobliżu (strefa prefetch). LazyColumn pojawił się w Compose 1.0 (2021) wraz z LazyRow (lista pozioma) i LazyVerticalGrid (siatka).

Leniwę ładowanie działa poprzez mechanizm SubcomposeLayout: LazyColumn mierzy dostępne miejsce, żąda z LayoutInfo widocznego zakresu i komponuje tylko elementy w tym zakresie. Elementy, które zniknęły z ekranu, opuszczają kompozycję (z wyjątkiem bufora prefetch). Według danych Google (Android Performance, 2026), LazyColumn zapewnia 60 FPS podczas przewijania listy 10000+ elementów na urządzeniach średniej klasy.

Różnica z Column + ScrollState

Column z pionowym przewijaniem — to zwykłe ułożenie wszystkich elementów jeden pod drugim z zastosowaniem modyfikatora verticalScroll. Column nie jest leniwy: wszystkie elementy potomne są komponowane od razu, nawet jeśli nie są widoczne. Dla listy 200+ elementów Column powoduje znaczne opóźnienie przy starcie. LazyColumn komponuje tylko widoczne + bufor (domyślnie 1 ekran w przód/tył), co czyni go niezastąpionym dla długich list.

items API: wiązanie danych z listą

items API — zestaw funkcji rozszerzających dla LazyColumn, które przyjmują listę danych i opis DSL dla każdego elementu. Główne funkcje: items(count, key, itemContent) dla stałej liczby, items(list, key, itemContent) dla listy, itemsIndexed(list, key, itemContent) z indeksem. Parametr key — obowiązkowy warunek wydajności — daje Compose stabilny identyfikator elementu.

kotlin
// Podstawowa LazyColumn z itemContent
@Composable
fun ArticleList(articles: List<Article>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header — nie leniwy, zawsze na początku
        item {
            Text("Ostatnie artykuły", style = MaterialTheme.typography.headlineMedium)
        }

        // Lista artykułów z kluczem po id
        items(articles, key = { it.id }) { article ->
            ArticleCard(
                title = article.title,
                summary = article.summary,
                onClick = { onArticleClick(article.id) }
            )
        }

        // Footer ze wskaźnikiem ładowania
        item {
            CircularProgressIndicator(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            )
        }
    }
}

// Niestandardowy element listy
@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(): używaj item {} dla pojedynczych elementów, które zawsze występują na liście (header, footer, separatory). Używaj items() dla dynamicznej listy danych. items() przyjmuje Iterable lub Int i blok DSL, który jest wywoływany dla każdego elementu. Łącz item i items w jednym LazyColumn: kolejność wywołań określa kolejność wyświetlania.

LazyListState: zarządzanie pozycją przewijania

LazyListState — obiekt przechowujący stan listy: bieżącą pozycję przewijania (firstVisibleItemIndex, firstVisibleItemScrollOffset), widoczne elementy (layoutInfo) i metody programowego przewijania (scrollToItem, animateScrollToItem). LazyListState tworzy się przez rememberLazyListState() i przekazuje do LazyColumn przez parametr state.

kotlin
// LazyColumn z zachowaniem pozycji przewijania
@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)
            }
        }

        // Przycisk "Do góry" pojawia się po 10. elemencie
        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, "Na górę")
            }
        }
    }
}

// Pobieranie informacji o widocznych elementach
@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("Wyświetlono $firstItem–$lastItem z $totalItems")
}

Programowe przewijanie wykonuje się przez suspend-funkcje listState: scrollToItem(index, scrollOffset) — natychmiastowe przesunięcie (bez animacji), animateScrollToItem(index) — z animacją. Do przewijania z korutyny używaj LaunchedEffect lub coroutineScope.launch. Ważne: nie wywołuj suspend-funkcji z kompozycji (wewnątrz @Composable) — używaj lambd (onClick, LaunchedEffect).

LazyColumn vs RecyclerView: porównanie podejść

LazyColumn i RecyclerView rozwiązują to samo zadanie — efektywne wyświetlanie długich list. Różnica w architekturze: RecyclerView używa ViewHolder i Adapter (klasyczny Android View), LazyColumn — deklaratywne funkcje composable bez XML i ViewHolder. Wybór zależy od stosu technologicznego projektu: Compose vs View-based UI.

ParametrLazyColumn (Compose)RecyclerView (View)
UkładFunkcje Composable (Kotlin DSL)Pliki XML + ViewBinding
Adapteritems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
Sortowanie/filtrowanieSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
AnimacjaAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
Różne typy elementówitem {}/items() + when po typiegetItemViewType + wiele ViewHolderów
Przewijanie do pozycjilistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchIn-process (SubcomposeLayout buffer)RecycledViewPool + GapWorker
Sieć/paginacjacollectAsLazyPagingItems() + Paging 3PagingDataAdapter + Paging 3

Zalecenie Google (Android Developers, 2026): dla nowych projektów na Jetpack Compose używaj LazyColumn. Dla projektów View-based używaj RecyclerView. LazyColumn wymaga Compose, który dodaje do APK około 3–5 MB — uwzględnij to przy wsparciu starych urządzeń. Jeśli projekt już używa systemu View, LazyColumn nie może współistnieć w jednym ekranie z układem XML bez AndroidView — w takich przypadkach łatwiej pozostać na RecyclerView.

LazyVerticalGrid i LazyHorizontalGrid: siatki

LazyVerticalGrid — wersja LazyColumn dla siatek o stałej liczbie kolumn (columns). LazyHorizontalGrid — pozioma siatka o stałej liczbie wierszy (rows). Oba komponenty używają tych samych zasad leniwego ładowania i tego samego items API co LazyColumn. Parametr columns określa liczbę kolumn przez GridCells.Fixed(N) lub GridCells.Adaptive(minSize).

kotlin
// Siatka z adaptacyjnymi kolumnami
@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 pozioma (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 automatycznie oblicza liczbę kolumn na podstawie minimalnego rozmiaru komórki. Na przykład GridCells.Adaptive(128.dp) na ekranie o szerokości 360 dp mieści 2 kolumny, na tablecie 600 dp — 4 kolumny. GridCells.Fixed(2) zawsze pokazuje dokładnie 2 kolumny. Adaptive jest preferowany do adaptacji wieloekranowej — automatycznie dostosowuje się do szerokości ekranu bez zapytań medialnych.

Wydajność LazyColumn: key, contentType, prefetch

Wydajność LazyColumn zależy od trzech czynników: stabilność kluczy (key), typ zawartości (contentType) i bufor prefetch (beyondBounds). Domyślnie LazyColumn buforuje 1 ekran w przód i 1 w tył. Google (Android Developers, 2026) zaleca konfigurowanie tych parametrów dla list z różnymi typami elementów lub złożoną kompozycją.

ParametrOpisZalecenie
keyStabilny identyfikator elementuZawsze podawaj key = { it.id }. Bez key Compose używa indeksu, a zmiana kolejności elementów psuje animację.
contentTypeTyp zawartości do podziału pulOkreśl dla list z różnymi typami elementów (tekst + obraz + reklama). Compose będzie ponownie używać kompozycji tylko w ramach jednego contentType.
beyondBoundsLiczba ekranów do prefetchZwiększ do 2–3 dla list z ciężkimi obrazami. Domyślnie 1 ekran.
Modifier.animateItemAnimacja zmiany pozycjiUżywaj do animowanego sortowania i filtrowania (Compose 1.7+).
kotlin
// Zoptymalizowana LazyColumn z contentType i prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        modifier = Modifier.fillMaxSize(),
        beyondBoundsPageCount = 2  // prefetch 2 ekrany
    ) {
        items(
            items = items,
            key = { it.id },
            contentType = { it.type }  // podział według typów
        ) { item ->
            when (item) {
                is FeedItem.Post -> PostView(item)
                is FeedItem.Ad -> AdView(item)
                is FeedItem.Suggested -> SuggestedView(item)
            }
        }
    }
}

// Pomiar wydajności przez listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
    val scrollInfo = listState.layoutInfo
    val visibleCount = scrollInfo.visibleItemsInfo.size
    val total = scrollInfo.totalItemsCount

    // derivedStateOf — nie przerzuca się, jeśli wartość się nie zmieniła
    val scrollProgress by remember {
        derivedStateOf {
            if (total > 0) visibleCount.toFloat() / total else 0f
        }
    }

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

Częste błędy: (1) brak key — elementy są ponownie komponowane przy każdej zmianie listy; (2) używanie mutableStateListOf bez strumieni snapshot — zmiany mogą nie być śledzone; (3) wywoływanie suspend-funkcji wewnątrz itemContent — przerywa kompozycję; (4) brak contentType dla mieszanych typów — Compose ponownie używa kompozycji reklamy dla posta tekstowego, powodując artefakty wizualne. W IT Sectr dodajemy contentType do wszystkich list z trzema i więcej typami zawartości.

Często zadawane pytania

LazyColumn przewija się wolno — co robić?

Sprawdź trzy parametry: (1) key — bez stabilnego klucza Compose nie może ponownie użyć kompozycji, (2) contentType — dla list z różnymi typami elementów, (3) beyondBoundsPageCount — zwiększ do 2 dla obrazów. Ciężkie obliczenia wewnątrz itemContent przenoś do remember z kluczem według danych. Używaj Modifier.drawWithContent zamiast Image dla prostej grafiki.

Jak dodać separator między elementami LazyColumn?

Używaj LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) dla odstępów między elementami. Dla wizualnego separatora z linią dodaj item { Divider() } między items. Automatyczny separator: items(items, key = { it.id }) { item -> ... }, a między każdym elementem wstaw item { HorizontalDivider() }. W tym celu użyj LazyListScope.items(list, key) { /* element */ } i osobno LazyListScope.item { Divider() }.

LazyColumn wewnątrz Column nie działa — dlaczego?

LazyColumn ma domyślnie nieskończoną wysokość (fillMaxSize). Wewnątrz Column bez stałej wysokości LazyColumn nie może określić rozmiaru. Rozwiązanie: (1) ustaw LazyColumn stałą wysokość: Modifier.height(400.dp), (2) użyj Modifier.weight(1f) wewnątrz Column, (3) nie umieszczaj LazyColumn w pionowo przewijanym kontenerze — używaj LazyColumn jako elementu głównego z item {} dla headera i footera.

Jak zaktualizować LazyColumn przy zmianie danych?

LazyColumn automatycznie reaguje na zmiany State. Jeśli dane są przechowywane w mutableStateListOf lub mutableStateOf, Compose ponownie komponuje tylko zmienione elementy. Dla list z ViewModel używaj collectAsState() z Flow. Przy aktualizacji listy z key Compose automatycznie animuje zmiany (dodawanie, usuwanie, przenoszenie) przez Modifier.animateItemPlacement().

LazyColumn vs LazyVerticalGrid — co wybrać?

LazyColumn — dla list z jedną kolumną (kanał, czat, komentarze). LazyVerticalGrid — dla siatek (galeria, katalog, ikony). Jeśli elementy mają być w jednej kolumnie na telefonie i w dwóch na tablecie — używaj LazyVerticalGrid z GridCells.Adaptive. Jeśli potrzebna jest mieszana siatka (różna liczba kolumn w jednej sekcji) — używaj LazyColumn i wewnątrz items wstawiaj poziome LazyRow lub zagnieżdżone LazyVerticalGrid.

Podsumowanie

  • LazyColumn — komponent Jetpack Compose do leniwych list pionowych z deklaratywnym API.
  • items() API przyjmuje listę i blok DSL dla każdego elementu; key jest obowiązkowy dla wydajności.
  • LazyListState zarządza pozycją przewijania, udostępnia suspend-funkcje do programowego przewijania.
  • contentType dzieli pule kompozycji dla list z różnymi typami elementów.
  • LazyVerticalGrid / LazyRow — warianty dla siatek i list poziomych z tym samym items API.
  • LazyColumn zapewnia 60 FPS dla 10000+ elementów dzięki SubcomposeLayout i buforowi prefetch.
  • Dla nowych projektów Compose używaj LazyColumn; dla projektów View-based — RecyclerView.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również