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, которая отображает вертикальный список элементов с ленивой (lazy) загрузкой: создаются и компонуются только те элементы, которые видны на экране или находятся рядом (prefetch zone). LazyColumn появился в Compose 1.0 (2021) вместе с LazyRow (горизонтальный список) и LazyVerticalGrid (сетка).
Ленивая загрузка работает через механизм SubcomposeLayout: LazyColumn измеряет доступное пространство, запрашивает у LayoutInfo видимый диапазон и составляет только элементы в этом диапазоне. Элементы, ушедшие за экран, покидают композицию (за исключением prefetch buffer). По данным Google (Android Performance, 2026), LazyColumn обеспечивает 60 FPS при скролле списка из 10000+ элементов на устройствах среднего сегмента.
Column с вертикальным scroll — это обычное расположение всех элементов друг под другом с применением модификатора 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 + multiple ViewHolders |
| Скролл к позиции | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | In-process (SubcomposeLayout buffer) | 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 для сеток с фиксированным количеством колонок (columns). LazyHorizontalGrid — горизонтальная сетка с фиксированными строками (rows). Оба компонента используют те же принципы ленивой загрузки и тот же 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также