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, који додаје у APK око 3–5 MB — узмите ово у обзир при подршци старих уређаја. Ако пројекат већ користи 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође