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, которая отображает вертикальный список элементов с ленивой (lazy) загрузкой: создаются и компонуются только те элементы, которые видны на экране или находятся рядом (prefetch zone). LazyColumn появился в Compose 1.0 (2021) вместе с LazyRow (горизонтальный список) и LazyVerticalGrid (сетка).

Ленивая загрузка работает через механизм SubcomposeLayout: LazyColumn измеряет доступное пространство, запрашивает у LayoutInfo видимый диапазон и составляет только элементы в этом диапазоне. Элементы, ушедшие за экран, покидают композицию (за исключением prefetch buffer). По данным Google (Android Performance, 2026), LazyColumn обеспечивает 60 FPS при скролле списка из 10000+ элементов на устройствах среднего сегмента.

Разница с Column + ScrollState

Column с вертикальным scroll — это обычное расположение всех элементов друг под другом с применением модификатора 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 + multiple ViewHolders
Скролл к позицииlistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchIn-process (SubcomposeLayout buffer)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 для сеток с фиксированным количеством колонок (columns). LazyHorizontalGrid — горизонтальная сетка с фиксированными строками (rows). Оба компонента используют те же принципы ленивой загрузки и тот же 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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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