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 και συνθέτει μόνο τα στοιχεία σε αυτό το εύρος. Τα στοιχεία που έχουν φύγει από την οθόνη εγκαταλείπουν τη σύνθεση (εκτός από το buffer prefetch). Σύμφωνα με δεδομένα της Google (Android Performance, 2026), το LazyColumn παρέχει 60 FPS κατά την κύλιση λίστας 10000+ στοιχείων σε συσκευές μεσαίας κατηγορίας.
Column με κάθετη κύλιση — είναι η συνήθης τοποθέτηση όλων των στοιχείων το ένα κάτω από το άλλο με εφαρμογή του τροποποιητή verticalScroll. Το Column δεν είναι τεμπέλικο: όλα τα θυγατρικά στοιχεία συντίθενται αμέσως, ακόμα κι αν δεν είναι ορατά. Για λίστα 200+ στοιχείων, το Column προκαλεί σημαντική καθυστέρηση κατά την εκκίνηση. Το LazyColumn συνθέτει μόνο τα ορατά στοιχεία + buffer (από προεπιλογή 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) — με κινούμενα σχέδια. Για κύλιση από coroutine χρησιμοποιήστε LaunchedEffect ή coroutineScope.launch. Σημαντικό: μην καλείτε συναρτήσεις suspend από τη σύνθεση (εντός @Composable) — χρησιμοποιήστε lambdas (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 |
| Adapter | 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 (buffer 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 στήλες, σε tablet 600 dp — 4 στήλες. GridCells.Fixed(2) εμφανίζει πάντα ακριβώς 2 στήλες. Το Adaptive είναι προτιμότερο για προσαρμογή πολλαπλών οθονών — προσαρμόζεται αυτόματα στο πλάτος οθόνης χωρίς ερωτήματα πολυμέσων.
Απόδοση του LazyColumn εξαρτάται από τρεις παράγοντες: σταθερότητα κλειδιών (key), τύπος περιεχομένου (contentType) και buffer 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 as ριζικό στοιχείο με item {} για header και footer.
LazyColumn αντιδρά αυτόματα σε αλλαγές State. Εάν τα δεδομένα αποθηκεύονται σε mutableStateListOf ή mutableStateOf, το Compose επανασυνθέτει μόνο τα στοιχεία που άλλαξαν. Για λίστες από ViewModel χρησιμοποιήστε collectAsState() με Flow. Κατά την ενημέρωση λίστας με key, το Compose κάνει αυτόματα κινούμενα σχέδια αλλαγών (προσθήκη, αφαίρεση, μετακίνηση) μέσω Modifier.animateItemPlacement().
LazyColumn — για λίστες μίας στήλης (feed, chat, σχόλια). LazyVerticalGrid — για πλέγματα (γαλλερία, κατάλογος, εικονίδια). Εάν τα στοιχεία πρέπει να είναι σε μία στήλη στο τηλέφωνο και δύο στήλες στο tablet — χρησιμοποιήστε LazyVerticalGrid με GridCells.Adaptive. Εάν χρειάζεστε μικτό πλέγμα (διαφορετικός αριθμός στηλών σε μία ενότητα) — χρησιμοποιήστε LazyColumn και εντός items εισάγετε οριζόντιο LazyRow ή ένθετο LazyVerticalGrid.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης