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 зона). LazyColumn се появи в Compose 1.0 (2021) заедно с LazyRow (хоризонтален списък) и LazyVerticalGrid (мрежа).
Мързеливо зареждане работи чрез механизма SubcomposeLayout: LazyColumn измерва наличното пространство, изисква видимия обхват от LayoutInfo и композира само елементите в този обхват. Елементите, които са напуснали екрана, напускат композицията (с изключение на prefetch буфера). Според данни на Google (Android Performance, 2026), LazyColumn осигурява 60 FPS при превъртане на списък от 10000+ елемента на устройства от среден клас.
Column с вертикално превъртане — обикновено подреждане на всички елементи един под друг с прилагане на модификатора verticalScroll. Column не е мързелив: всички дъщерни елементи се композират веднага, дори ако не са видими. За списък от 200+ елемента, Column причинява значително забавяне при стартиране. LazyColumn композира само видимите елементи + буфер (по подразбиране 1 екран напред/назад), което го прави незаменим за дълги списъци.
items API — набор от разширяващи функции за 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 + множество ViewHolder-и |
| Превъртане до позиция | 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, който добавя около 3–5 MB към APK — вземете това предвид при поддръжка на стари устройства. Ако проектът вече използва 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) и prefetch буфер (beyondBounds). По подразбиране LazyColumn буферира 1 екран напред и 1 назад. Google (Android Developers, 2026) препоръчва конфигуриране на тези параметри за списъци с различни типове елементи или сложна композиция.
| Параметър | Описание | Препоръка |
|---|---|---|
| key | Стабилен идентификатор на елемента | Винаги посочвайте key = { it.id }. Без key Compose използва индекс и пренареждането на елементи разваля анимацията. |
| contentType | Тип на съдържанието за разделяне на пулове | Посочете за списъци с различни типове елементи (текст + изображение + реклама). Compose ще използва повторно композиция само в рамките на един contentType. |
| beyondBounds | Брой екрани за prefetch | Увеличете до 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също