LazyColumn — مكون Jetpack Compose لعرض القوائم القابلة للتمرير مع التحميل البطيء. على عكس RecyclerView، لا ينشئ LazyColumn ViewHolders ولا يستخدم XML — يتم وصف جميع العناصر بشكل تصريحي من خلال دوال composable. نعرض ما هو LazyColumn، وكيف يعمل LazyListState وواجهة items API، وكيفية تحقيق أداء 60 إطارًا في الثانية. في IT Sectr نستخدم LazyColumn في جميع مشاريع Compose الجديدة. للمقارنة مع النهج الكلاسيكي، اقرأ مقالة عن RecyclerView.
الرئيسية
items() وitemsIndexed() وitems() مع المفاتيح لربط البيانات.LazyColumn هي دالة composable من مكتبة Foundation Compose تعرض قائمة رأسية من العناصر مع التحميل البطيء: يتم إنشاء وتركيب العناصر المرئية على الشاشة أو القريبة منها فقط (منطقة التحميل المسبق). ظهر LazyColumn في Compose 1.0 (2021) إلى جانب LazyRow (قائمة أفقية) وLazyVerticalGrid (شبكة).
التحميل البطيء يعمل من خلال آلية SubcomposeLayout: يقيس LazyColumn المساحة المتاحة، ويطلب النطاق المرئي من LayoutInfo، ويقوم بتركيب العناصر في هذا النطاق فقط. العناصر التي تخرج عن الشاشة تغادر التركيب (باستثناء مخزن التحميل المسبق). وفقًا لـ Google (أداء Android، 2026)، يحافظ LazyColumn على 60 إطارًا في الثانية عند التمرير خلال قائمة من 10000+ عنصر على أجهزة الفئة المتوسطة.
Column مع التمرير الرأسي هو ترتيب عادي لجميع العناصر واحدًا تحت الآخر مع تطبيق معدِّل verticalScroll. Column ليست بطيئة: جميع العناصر التابعة تُركب فورًا حتى لو لم تكن مرئية. لقائمة من 200+ عنصر، تسبب Column تأخيرًا كبيرًا عند بدء التشغيل. يقوم LazyColumn بتركيب العناصر المرئية فقط + المخزن المؤقت (شاشة واحدة للأمام/للخلف افتراضيًا)، مما يجعله لا غنى عنه للقوائم الطويلة.
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 {} ضد items(): استخدم item {} للعناصر المفردة الموجودة دائمًا في القائمة (رأس، تذييل، فواصل). استخدم 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)
}
}
// زر "لأعلى" يظهر بعد العنصر العاشر
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) — استخدم لامبدا (onClick, LaunchedEffect).
LazyColumn وRecyclerView يحلان نفس المشكلة — العرض الفعال للقوائم الطويلة. الفرق في البنية: RecyclerView يستخدم ViewHolder وAdapter (عرض Android الكلاسيكي)، LazyColumn يستخدم دوال composable تصريحية بدون XML أو ViewHolder. يعتمد الاختيار على حزمة التقنيات للمشروع: Compose مقابل واجهة المستخدم القائمة على View.
| المعامل | 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 + ViewHolders متعددة |
| التمرير إلى موضع | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| التحميل المسبق | داخل العملية (مخزن SubcomposeLayout) | RecycledViewPool + GapWorker |
| الشبكة/التصفح | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
توصية Google (Android Developers, 2026): للمشاريع الجديدة على Jetpack Compose، استخدم LazyColumn. للمشاريع القائمة على View، استخدم RecyclerView. يتطلب LazyColumn Compose، الذي يضيف حوالي 3–5 ميجابايت إلى APK — ضع ذلك في الاعتبار عند دعم الأجهزة القديمة. إذا كان المشروع يستخدم بالفعل نظام View، فلا يمكن لـ LazyColumn التعايش في شاشة واحدة مع تخطيط XML بدون AndroidView — في مثل هذه الحالات، من الأسهل البقاء مع RecyclerView.
LazyVerticalGrid — إصدار من LazyColumn للشبكات بعدد ثابت من الأعمدة. LazyHorizontalGrid — شبكة أفقية بعدد ثابت من الصفوف. يستخدم كلا المكونين نفس مبادئ التحميل البطيء ونفس واجهة 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 تضع عمودين، على جهاز لوحي 600 dp — 4 أعمدة. GridCells.Fixed(2) يعرض دائمًا عمودين بالضبط. يُفضل Adaptive للتكيف مع الشاشات المتعددة — يضبط تلقائيًا عرض الشاشة بدون استعلامات وسائط.
أداء LazyColumn يعتمد على ثلاثة عوامل: استقرار المفتاح (key)، نوع المحتوى (contentType)، ومخزن التحميل المسبق (beyondBounds). افتراضيًا، يخزن LazyColumn شاشة واحدة للأمام وشاشة واحدة للخلف. توصي Google (Android Developers, 2026) بتكوين هذه المعاملات للقوائم بأنواع عناصر مختلفة أو تركيب معقد.
| المعامل | الوصف | التوصية |
|---|---|---|
| key | معرف ثابت للعنصر | حدد دائمًا key = { it.id }. بدون key، يستخدم Compose الفهرس، وإعادة ترتيب العناصر يكسر الرسوم المتحركة. |
| contentType | نوع المحتوى لفصل المجموعات | حدد للقوائم بأنواع عناصر مختلفة (نص + صورة + إعلان). سيعيد Compose استخدام التركيب فقط داخل نفس contentType. |
| beyondBounds | عدد شاشات التحميل المسبق | زده إلى 2–3 للقوائم ذات الصور الثقيلة. الافتراضي هو شاشة واحدة. |
| 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 // تحميل مسبق لشاشتين
) {
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 {} للرأس والتذييل.
LazyColumn يتفاعل تلقائيًا مع تغييرات State. إذا كانت البيانات مخزنة في mutableStateListOf أو mutableStateOf، يعيد Compose تركيب العناصر المتغيرة فقط. للقوائم من ViewModel، استخدم collectAsState() مع Flow. عند تحديث القائمة باستخدام key، يقوم Compose تلقائيًا بتحريك التغييرات (إضافة، إزالة، إعادة ترتيب) عبر Modifier.animateItemPlacement().
LazyColumn — للقوائم بعمود واحد (تغذية، دردشة، تعليقات). LazyVerticalGrid — للشبكات (معرض، كتالوج، أيقونات). إذا كان يجب أن تكون العناصر في عمود واحد على الهاتف وعمودين على الجهاز اللوحي — استخدم LazyVerticalGrid مع GridCells.Adaptive. إذا كانت هناك حاجة لشبكة مختلطة (عدد أعمدة مختلف في قسم واحد) — استخدم LazyColumn وأدخل LazyRow أفقي أو LazyVerticalGrid متداخلة داخل items.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.