LazyColumn — що це, ефективні списки в Compose

Автор: IT Sectr Опубліковано: 2026-02-24 Час читання: 10 хв

LazyColumn — компонент Jetpack Compose для відображення прокручуваних списків із лінивим завантаженням. На відміну від RecyclerView, LazyColumn не створює ViewHolder'и та не використовує XML — усі елементи описуються декларативно через composable-функції. Показуємо, що таке LazyColumn, як працює LazyListState та items API, і як досягти продуктивності на рівні 60 FPS. В IT Sectr ми використовуємо LazyColumn у всіх нових Compose-проєктах. Для порівняння з класичним підходом читайте статтю про RecyclerView.

Головне

  • LazyColumn — компонент Jetpack Compose для вертикальних списків із лінивим завантаженням, частина Material Design 3.
  • LazyListState — стан списку: позиція скролу, видимі елементи, програмне прокручування.
  • items API — функції items(), itemsIndexed(), items() з ключами для прив'язки даних.
  • key — параметр для стабільної ідентифікації елементів, що дозволяє Compose перевикористовувати композицію.
  • LazyVerticalGrid — версія LazyColumn для сіток із фіксованою кількістю колонок.

Що таке LazyColumn в Jetpack Compose?

LazyColumn — це composable-функція з бібліотеки Foundation Compose, яка відображає вертикальний список елементів із лінивим завантаженням: створюються та компонуються лише ті елементи, які видно на екрані або знаходяться поруч (зона попереднього завантаження). LazyColumn з'явився в Compose 1.0 (2021) разом із LazyRow (горизонтальний список) та LazyVerticalGrid (сітка).

Ліниве завантаження працює через механізм SubcomposeLayout: LazyColumn вимірює доступний простір, запитує у LayoutInfo видимий діапазон і компонує лише елементи в цьому діапазоні. Елементи, які вийшли за екран, залишають композицію (за винятком буфера попереднього завантаження). За даними Google (Android Performance, 2026), LazyColumn забезпечує 60 FPS під час прокручування списку з 10000+ елементів на пристроях середнього сегмента.

Різниця з Column + ScrollState

Column із вертикальним скролом — це звичайне розташування всіх елементів один під одним із застосуванням модифікатора verticalScroll. Column не лінивий: усі дочірні елементи компонуються відразу, навіть якщо вони не видимі. Для списку з 200+ елементів Column викликає значний лаг під час запуску. LazyColumn компонує лише видимі елементи + буфер (за замовчуванням 1 екран вперед/назад), що робить його незамінним для довгих списків.

items API: прив'язка даних до списку

items API — набір extension-функцій для LazyColumn, які приймають список даних та DSL-описувач для кожного елемента. Основні функції: items(count, key, itemContent) для фіксованої кількості, items(list, key, itemContent) для списку, itemsIndexed(list, key, itemContent) з індексом. Параметр key — обов'язкова умова для продуктивності — дає Compose стабільний ідентифікатор елемента.

kotlin
// Базова LazyColumn з itemContent
@Composable
fun ArticleList(articles: List<Article>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header — не лінивий, завжди на початку
        item {
            Text("Останні статті", style = MaterialTheme.typography.headlineMedium)
        }

        // Список статей з ключем за id
        items(articles, key = { it.id }) { article ->
            ArticleCard(
                title = article.title,
                summary = article.summary,
                onClick = { onArticleClick(article.id) }
            )
        }

        // Footer з індикатором завантаження
        item {
            CircularProgressIndicator(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            )
        }
    }
}

// Кастомний елемент списку
@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(): використовуйте item {} для одиночних елементів, які завжди присутні в списку (header, footer, розділювачі). Використовуйте items() для динамічного списку даних. items() приймає Iterable або Int і DSL-блок, який викликається для кожного елемента. Комбінуйте item та items в одному LazyColumn: порядок викликів визначає порядок відображення.

LazyListState: управління позицією скролу

LazyListState — об'єкт, що зберігає стан списку: поточну позицію скролу (firstVisibleItemIndex, firstVisibleItemScrollOffset), видимі елементи (layoutInfo) та методи програмного прокручування (scrollToItem, animateScrollToItem). LazyListState створюється через rememberLazyListState() і передається в LazyColumn через параметр state.

kotlin
// LazyColumn зі збереженням позиції скролу
@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)
            }
        }

        // Кнопка "Вгору" з'являється після 10-го елемента
        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, "Нагору")
            }
        }
    }
}

// Отримання інформації про видимі елементи
@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("Показано $firstItem–$lastItem з $totalItems")
}

Програмне прокручування виконується через suspend-функції listState: scrollToItem(index, scrollOffset) — миттєве переміщення (без анімації), animateScrollToItem(index) — з анімацією. Для прокручування з корутини використовуйте LaunchedEffect або coroutineScope.launch. Важливо: не викликайте suspend-функції з композиції (всередині @Composable) — використовуйте лямбди (onClick, LaunchedEffect).

LazyColumn vs RecyclerView: порівняння підходів

LazyColumn та RecyclerView вирішують одну задачу — ефективне відображення довгих списків. Різниця в архітектурі: RecyclerView використовує ViewHolder та Adapter (класичний Android View), LazyColumn — декларативні composable-функції без XML та ViewHolder. Вибір залежить від стеку технологій проекту: Compose vs View-based UI.

ПараметрLazyColumn (Compose)RecyclerView (View)
РозміткаComposable-функції (Kotlin DSL)XML-файли + ViewBinding
Адаптерitems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
Сортування/фільтраціяSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
АнімаціяAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
Різні типи елементівitem {}/items() + when за типомgetItemViewType + декілька ViewHolders
Скрол до позиціїlistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchIn-process (буфер SubcomposeLayout)RecycledViewPool + GapWorker
Мережа/пагінаціяcollectAsLazyPagingItems() + Paging 3PagingDataAdapter + Paging 3

Рекомендація Google (Android Developers, 2026): для нових проєктів на Jetpack Compose використовуйте LazyColumn. Для View-based проєктів використовуйте RecyclerView. LazyColumn потребує Compose, який додає в APK близько 3–5 МБ — враховуйте це при підтримці старих пристроїв. Якщо проєкт уже використовує View-систему, LazyColumn не може співіснувати на одному екрані з XML-розміткою без AndroidView — у таких випадках простіше залишитися на RecyclerView.

LazyVerticalGrid та LazyHorizontalGrid: сітки

LazyVerticalGrid — версія LazyColumn для сіток із фіксованою кількістю колонок. LazyHorizontalGrid — горизонтальна сітка з фіксованими рядками. Обидва компоненти використовують ті самі принципи лінивого завантаження та той самий items API, що й LazyColumn. Параметр columns визначає кількість колонок через GridCells.Fixed(N) або GridCells.Adaptive(minSize).

kotlin
// Сітка з адаптивними колонками
@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)
            )
        }
    }
}

// Горизонтальний список (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 автоматично обчислює кількість колонок на основі мінімального розміру комірки. Наприклад, GridCells.Adaptive(128.dp) на екрані шириною 360 dp розміщує 2 колонки, на планшеті 600 dp — 4 колонки. GridCells.Fixed(2) завжди показує рівно 2 колонки. Adaptive кращий для мультиекранної адаптації — він автоматично підлаштовується під ширину екрана без медіа-запитів.

Продуктивність LazyColumn: key, contentType, prefetch

Продуктивність LazyColumn залежить від трьох факторів: стабільність ключів (key), тип контенту (contentType) та буфер попереднього завантаження (beyondBounds). За замовчуванням LazyColumn буферизує 1 екран вперед і 1 назад. Google (Android Developers, 2026) рекомендує налаштовувати ці параметри для списків із різними типами елементів або складною композицією.

ПараметрОписРекомендація
keyСтабільний ідентифікатор елементаЗавжди вказуйте key = { it.id }. Без key Compose використовує індекс, і перестановка елементів ламає анімацію.
contentTypeТип контенту для розділення пулівВкажіть для списків із різними типами елементів (текст + зображення + реклама). Compose буде перевикористовувати композицію лише всередині одного contentType.
beyondBoundsКількість екранів для попереднього завантаженняЗбільште до 2–3 для списків із важкими зображеннями. За замовчуванням 1 екран.
Modifier.animateItemАнімація зміни позиціїВикористовуйте для анімованого сортування та фільтрації (Compose 1.7+).
kotlin
// Оптимізований LazyColumn з contentType та prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        modifier = Modifier.fillMaxSize(),
        beyondBoundsPageCount = 2  // prefetch 2 екрани
    ) {
        items(
            items = items,
            key = { it.id },
            contentType = { it.type }  // розділення за типами
        ) { item ->
            when (item) {
                is FeedItem.Post -> PostView(item)
                is FeedItem.Ad -> AdView(item)
                is FeedItem.Suggested -> SuggestedView(item)
            }
        }
    }
}

// Вимірювання продуктивності через listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
    val scrollInfo = listState.layoutInfo
    val visibleCount = scrollInfo.visibleItemsInfo.size
    val total = scrollInfo.totalItemsCount

    // derivedStateOf — не перекомпоновується, якщо значення не змінилося
    val scrollProgress by remember {
        derivedStateOf {
            if (total > 0) visibleCount.toFloat() / total else 0f
        }
    }

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

Поширені помилки: (1) відсутність key — елементи перекомпоновуються при будь-якій зміні списку; (2) використання mutableStateListOf без snapshot-потоків — зміни можуть не відстежуватися; (3) виклик suspend-функцій всередині itemContent — перериває композицію; (4) без contentType для змішаних типів — Compose перевикористовує композицію реклами для текстових постів, викликаючи візуальні артефакти. В IT Sectr ми додаємо contentType у всі списки з трьома і більше типами контенту.

Часті запитання

LazyColumn повільно прокручується — що робити?

Перевірте три параметри: (1) key — без стабільного ключа Compose не може перевикористовувати композицію, (2) contentType — для списків із різними типами елементів, (3) beyondBoundsPageCount — збільште до 2 для зображень. Важкі обчислення всередині itemContent переносьте в remember із ключем за даними. Використовуйте Modifier.drawWithContent замість Image для простої графіки.

Як додати розділювач між елементами LazyColumn?

Використовуйте LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) для відступів між елементами. Для візуального розділювача з лінією додайте item { Divider() } між items. Автоматичний розділювач: items(items, key = { it.id }) { item -> ... }, а між кожним елементом вставте item { HorizontalDivider() }. Для цього використовуйте LazyListScope.items(list, key) { /* елемент */ } та окремо LazyListScope.item { Divider() }.

LazyColumn всередині Column не працює — чому?

LazyColumn має нескінченну висоту (fillMaxSize) за замовчуванням. Всередині Column без фіксованої висоти LazyColumn не може визначити розмір. Рішення: (1) задайте LazyColumn фіксовану висоту: Modifier.height(400.dp), (2) використовуйте Modifier.weight(1f) всередині Column, (3) не вкладайте LazyColumn у вертикально прокручуваний контейнер — використовуйте LazyColumn як кореневий елемент із item {} для header та footer.

Як оновити LazyColumn при зміні даних?

LazyColumn автоматично реагує на зміну State. Якщо дані зберігаються в mutableStateListOf або mutableStateOf, Compose перекомпоновує лише змінені елементи. Для списків із ViewModel використовуйте collectAsState() із Flow. При оновленні списку з key Compose автоматично анімує зміни (додавання, видалення, переміщення) через Modifier.animateItemPlacement().

LazyColumn vs LazyVerticalGrid — що вибрати?

LazyColumn — для списків з однією колонкою (стрічка, чат, коментарі). LazyVerticalGrid — для сіток (галерея, каталог, іконки). Якщо елементи мають бути в одну колонку на телефоні та в дві на планшеті — використовуйте LazyVerticalGrid із GridCells.Adaptive. Якщо потрібна змішана сітка (різна кількість колонок в одній секції) — використовуйте LazyColumn і всередину items вставляйте горизонтальні LazyRow або вкладені LazyVerticalGrid.

Підсумки

  • LazyColumn — компонент Jetpack Compose для лінивих вертикальних списків із декларативним API.
  • items() API приймає список і DSL-блок для кожного елемента; key обов'язковий для продуктивності.
  • LazyListState керує позицією скролу, надає suspend-функції для програмного прокручування.
  • contentType розділяє пули композиції для списків із різними типами елементів.
  • LazyVerticalGrid / LazyRow — варіації для сіток і горизонтальних списків із тим же items API.
  • LazyColumn забезпечує 60 FPS для 10000+ елементів завдяки SubcomposeLayout та prefetch-буферу.
  • Для нових Compose-проєктів використовуйте LazyColumn; для View-based проєктів — RecyclerView.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також