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 هنگام پیمایش لیستی از 10000+ عنصر در دستگاههای میانرده، 60 FPS را تضمین میکند.
Column با پیمایش عمودی — چیدمان معمولی همه عناصر یکی پس از دیگری با اعمال modifier 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(): برای عناصر تکی که همیشه در لیست حضور دارند (header، footer، جداکنندهها) از item {} استفاده کنید. برای لیست داده پویا از items() استفاده کنید. items() Iterable یا Int و یک بلوک DSL که برای هر عنصر فراخوانی میشود را میپذیرد. item و items را در یک LazyColumn ترکیب کنید: ترتیب فراخوانیها ترتیب نمایش را تعیین میکند.
LazyListState — شیء ذخیرهکننده وضعیت لیست: موقعیت فعلی پیمایش (firstVisibleItemIndex، firstVisibleItemScrollOffset)، عناصر قابل مشاهده (layoutInfo) و روشهای پیمایش برنامهریزی شده (scrollToItem، animateScrollToItem). LazyListState از طریق rememberLazyListState() ایجاد میشود و از طریق پارامتر state به LazyColumn منتقل میشود.
// 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)
}
}
// دکمه "به بالا" بعد از دهمین عنصر ظاهر میشود
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 لیستState انجام میشود: scrollToItem(index, scrollOffset) — جابجایی فوری (بدون انیمیشن)، animateScrollToItem(index) — با انیمیشن. برای پیمایش از کوروتین از LaunchedEffect یا coroutineScope.launch استفاده کنید. مهم: توابع suspend را از داخل ترکیب (داخل @Composable) فراخوانی نکنید — از لامبداها (onClick، LaunchedEffect) استفاده کنید.
LazyColumn و RecyclerView یک وظیفه را حل میکنند — نمایش کارآمد لیستهای طولانی. تفاوت در معماری است: RecyclerView از ViewHolder و Adapter (Android View کلاسیک) استفاده میکند، LazyColumn — توابع composable اعلامی بدون XML و ViewHolder. انتخاب به stack فناوری پروژه بستگی دارد: 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 مگابایت به APK اضافه میکند — هنگام پشتیبانی از دستگاههای قدیمی این را در نظر بگیرید. اگر پروژه از سیستم View استفاده میکند، LazyColumn بدون AndroidView نمیتواند در یک صفحه با چیدمان XML共存 داشته باشد — در چنین مواردی ماندن روی 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 } را مشخص کنید. بدون کلید 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 منتقل کنید. برای گرافیک ساده به جای Image از Modifier.drawWithContent استفاده کنید.
برای فاصله بین عناصر از 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) داخل Column از Modifier.weight(1f) استفاده کنید، (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 از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید