LazyColumn — компонент Jetpack Compose для відображення прокручуваних списків із лінивим завантаженням. На відміну від RecyclerView, LazyColumn не створює ViewHolder'и та не використовує XML — усі елементи описуються декларативно через composable-функції. Показуємо, що таке LazyColumn, як працює LazyListState та items API, і як досягти продуктивності на рівні 60 FPS. В IT Sectr ми використовуємо LazyColumn у всіх нових Compose-проєктах. Для порівняння з класичним підходом читайте статтю про RecyclerView.
Головне
items(), itemsIndexed(), items() з ключами для прив'язки даних.LazyColumn — це composable-функція з бібліотеки Foundation Compose, яка відображає вертикальний список елементів із лінивим завантаженням: створюються та компонуються лише ті елементи, які видно на екрані або знаходяться поруч (зона попереднього завантаження). LazyColumn з'явився в Compose 1.0 (2021) разом із LazyRow (горизонтальний список) та LazyVerticalGrid (сітка).
Ліниве завантаження працює через механізм SubcomposeLayout: LazyColumn вимірює доступний простір, запитує у LayoutInfo видимий діапазон і компонує лише елементи в цьому діапазоні. Елементи, які вийшли за екран, залишають композицію (за винятком буфера попереднього завантаження). За даними Google (Android Performance, 2026), LazyColumn забезпечує 60 FPS під час прокручування списку з 10000+ елементів на пристроях середнього сегмента.
Column із вертикальним скролом — це звичайне розташування всіх елементів один під одним із застосуванням модифікатора verticalScroll. Column не лінивий: усі дочірні елементи компонуються відразу, навіть якщо вони не видимі. Для списку з 200+ елементів Column викликає значний лаг під час запуску. LazyColumn компонує лише видимі елементи + буфер (за замовчуванням 1 екран вперед/назад), що робить його незамінним для довгих списків.
items API — набір extension-функцій для LazyColumn, які приймають список даних та DSL-описувач для кожного елемента. Основні функції: items(count, key, itemContent) для фіксованої кількості, items(list, key, itemContent) для списку, itemsIndexed(list, key, itemContent) з індексом. Параметр key — обов'язкова умова для продуктивності — дає Compose стабільний ідентифікатор елемента.
// Базова 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 — об'єкт, що зберігає стан списку: поточну позицію скролу (firstVisibleItemIndex, firstVisibleItemScrollOffset), видимі елементи (layoutInfo) та методи програмного прокручування (scrollToItem, animateScrollToItem). LazyListState створюється через rememberLazyListState() і передається в LazyColumn через параметр state.
// 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 та 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() DSL | RecyclerView.Adapter + ViewHolder |
| Сортування/фільтрація | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Анімація | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Різні типи елементів | item {}/items() + when за типом | getItemViewType + декілька ViewHolders |
| Скрол до позиції | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | In-process (буфер SubcomposeLayout) | RecycledViewPool + GapWorker |
| Мережа/пагінація | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
Рекомендація Google (Android Developers, 2026): для нових проєктів на Jetpack Compose використовуйте LazyColumn. Для View-based проєктів використовуйте RecyclerView. LazyColumn потребує Compose, який додає в APK близько 3–5 МБ — враховуйте це при підтримці старих пристроїв. Якщо проєкт уже використовує View-систему, LazyColumn не може співіснувати на одному екрані з XML-розміткою без AndroidView — у таких випадках простіше залишитися на RecyclerView.
LazyVerticalGrid — версія LazyColumn для сіток із фіксованою кількістю колонок. LazyHorizontalGrid — горизонтальна сітка з фіксованими рядками. Обидва компоненти використовують ті самі принципи лінивого завантаження та той самий items API, що й LazyColumn. Параметр columns визначає кількість колонок через GridCells.Fixed(N) або GridCells.Adaptive(minSize).
// Сітка з адаптивними колонками
@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) та буфер попереднього завантаження (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+). |
// Оптимізований 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 у всі списки з трьома і більше типами контенту.
Часті запитання
Перевірте три параметри: (1) key — без стабільного ключа Compose не може перевикористовувати композицію, (2) contentType — для списків із різними типами елементів, (3) beyondBoundsPageCount — збільште до 2 для зображень. Важкі обчислення всередині itemContent переносьте в remember із ключем за даними. Використовуйте Modifier.drawWithContent замість Image для простої графіки.
Використовуйте 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 має нескінченну висоту (fillMaxSize) за замовчуванням. Всередині Column без фіксованої висоти LazyColumn не може визначити розмір. Рішення: (1) задайте LazyColumn фіксовану висоту: Modifier.height(400.dp), (2) використовуйте Modifier.weight(1f) всередині Column, (3) не вкладайте LazyColumn у вертикально прокручуваний контейнер — використовуйте LazyColumn як кореневий елемент із item {} для header та footer.
LazyColumn автоматично реагує на зміну State. Якщо дані зберігаються в mutableStateListOf або mutableStateOf, Compose перекомпоновує лише змінені елементи. Для списків із ViewModel використовуйте collectAsState() із Flow. При оновленні списку з key Compose автоматично анімує зміни (додавання, видалення, переміщення) через Modifier.animateItemPlacement().
LazyColumn — для списків з однією колонкою (стрічка, чат, коментарі). LazyVerticalGrid — для сіток (галерея, каталог, іконки). Якщо елементи мають бути в одну колонку на телефоні та в дві на планшеті — використовуйте LazyVerticalGrid із GridCells.Adaptive. Якщо потрібна змішана сітка (різна кількість колонок в одній секції) — використовуйте LazyColumn і всередину items вставляйте горизонтальні LazyRow або вкладені LazyVerticalGrid.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.