تحتاج العديد من التطبيقات إلى عرض مجموعات من العناصر. يشرح هذا المستند كيفية تنفيذ ذلك بكفاءة في Jetpack Compose.
إذا كنت تعرف أنّ حالة الاستخدام لا تتطلّب أي تمرير، يمكنك
استخدام Column أو Row بسيط (حسب الاتجاه)، وإصدار محتوى كل عنصر من خلال
التكرار على قائمة بالطريقة التالية:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
يمكننا جعل Column قابلاً للتمرير باستخدام المُعدِّل verticalScroll().
القوائم البطيئة
إذا كنت بحاجة إلى عرض عدد كبير من العناصر (أو قائمة بطول غير معروف)،
فإن استخدام تنسيق مثل Column يمكن أن يسبب مشاكل في الأداء، حيث سيتم إنشاء جميع العناصر وتخطيطها سواء كانت مرئية أم لا.
توفّر Compose مجموعة من المكوّنات التي تنشئ وتنسّق العناصر التي تظهر في إطار العرض الخاص بالمكوّن فقط. وتشمل هذه المكوّنات
LazyColumn
وLazyRow.
كما يشير الاسم، يكمن الفرق بين
LazyColumn
و
LazyRow
في الاتجاه الذي يتم فيه عرض العناصر والتمرير. LazyColumn
تُنشئ قائمة يتم التنقّل فيها عموديًا، وLazyRow تُنشئ قائمة يتم التنقّل فيها أفقيًا.
تختلف مكوّنات Lazy عن معظم التنسيقات في Compose. بدلاً من
قبول مَعلمة حظر المحتوى @Composable، ما يسمح للتطبيقات
بإصدار دوال مركّبة مباشرةً، توفّر مكوّنات Lazy حظر LazyListScope.(). توفّر مجموعة الرموز البرمجية
LazyListScope
هذه لغة DSL تتيح للتطبيقات وصف محتوى العناصر. بعد ذلك، يكون مكوّن Lazy مسؤولاً عن إضافة محتوى كل عنصر وفقًا لما يتطلّبه التنسيق وموضع الانتقال.
LazyListScope DSL
توفّر لغة DSL في LazyListScope عددًا من الدوال لوصف العناصر
في التنسيق. بشكل أساسي،
تضيف الدالة item()
عنصرًا واحدًا، وتضيف الدالة
items(Int)
عناصر متعددة:
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
هناك أيضًا عدد من دوال الإضافة التي تتيح لك إضافة
مجموعات من العناصر، مثل List. تتيح لنا هذه الإضافات نقل مثال Column أعلاه بسهولة:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
هناك أيضًا صيغة من دالة الإضافة
items()
تُسمى
itemsIndexed()،
وهي توفّر الفهرس. يُرجى الاطّلاع على مرجع
LazyListScope
لمزيد من التفاصيل.
الشبكات الكسولة
توفّر الدالتان المركّبتان
LazyVerticalGrid
و
LazyHorizontalGrid
إمكانية عرض العناصر في شبكة. ستعرض الشبكة العمودية الكسولة
عناصرها في حاوية قابلة للتمرير عموديًا، وتمتد عبر
أعمدة متعددة، بينما سيكون للشبكات الأفقية الكسولة نفس السلوك
على المحور الأفقي.
تتضمّن الشبكات إمكانات واجهة برمجة التطبيقات الفعّالة نفسها التي تتضمّنها القوائم، كما أنّها تستخدم
لغة DSL مشابهة جدًا، وهي
LazyGridScope.()
لوصف المحتوى.
تتحكّم المَعلمة columns في
LazyVerticalGrid
والمَعلمة rows في
LazyHorizontalGrid
في كيفية تشكيل الخلايا في أعمدة أو صفوف. يعرض المثال التالي العناصر في شبكة، باستخدام GridCells.Adaptive لضبط كل عمود ليكون بعرض 128.dp على الأقل:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
يتيح لك LazyVerticalGrid تحديد عرض للعناصر، ثم ستقوم الشبكة
بملاءمة أكبر عدد ممكن من الأعمدة. يتم توزيع أي عرض متبقٍ بالتساوي
بين الأعمدة، بعد حساب عدد الأعمدة.
هذه الطريقة التكيُّفية لتحديد الحجم مفيدة بشكل خاص لعرض مجموعات من العناصر
على أحجام شاشات مختلفة.
إذا كنت تعرف عدد الأعمدة المحدّد المطلوب استخدامه، يمكنك بدلاً من ذلك تقديم
مثيل من
GridCells.Fixed
يحتوي على عدد الأعمدة المطلوبة.
إذا كان تصميمك يتطلب فقط عناصر معينة ذات أبعاد غير قياسية،
يمكنك استخدام دعم الشبكة لتوفير امتدادات أعمدة مخصصة للعناصر.
حدِّد نطاق العمود باستخدام المَعلمة span في الطريقتَين
LazyGridScope DSL
item وitems.
maxLineSpan،
وهي إحدى قيم نطاق الامتداد، مفيدة بشكل خاص عند استخدام
الحجم التكيُّفي، لأنّ عدد الأعمدة ليس ثابتًا.
يوضّح هذا المثال كيفية توفير امتداد صف كامل:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
شبكة متداخلة بطيئة
LazyVerticalStaggeredGrid وLazyHorizontalStaggeredGrid هما دالتان مركّبتان تتيحان لك إنشاء شبكة متداخلة من العناصر يتم تحميلها بشكل بطيء.
تعرض الشبكة العمودية المتداخلة البطيئة عناصرها في حاوية قابلة للتمرير عموديًا
تمتد عبر أعمدة متعددة وتسمح للعناصر الفردية بأن تكون
بارتفاعات مختلفة. تتّبع الشبكات الأفقية البطيئة السلوك نفسه على
المحور الأفقي مع عناصر ذات عروض مختلفة.
المقتطف التالي هو مثال أساسي على استخدام LazyVerticalStaggeredGrid
مع عرض 200.dp لكل عنصر:
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(200.dp), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
لضبط عدد ثابت من الأعمدة، يمكنك استخدام
StaggeredGridCells.Fixed(columns) بدلاً من StaggeredGridCells.Adaptive.
يؤدي ذلك إلى تقسيم العرض المتاح على عدد الأعمدة (أو الصفوف في الشبكة الأفقية)، ويشغل كل عنصر هذا العرض (أو الارتفاع في الشبكة الأفقية):
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(3), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
حشو المحتوى
في بعض الأحيان، ستحتاج إلى إضافة مساحة متروكة حول حواف المحتوى. تتيح لك المكوّنات الكسولة تمرير بعض
PaddingValues
إلى المَعلمة contentPadding لإتاحة ذلك:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
في هذا المثال، نضيف 16.dp من المساحة المتروكة إلى الحواف الأفقية (اليسار واليمين)، ثم 8.dp إلى أعلى وأسفل المحتوى.
يُرجى العِلم أنّ هذه المساحة المتروكة يتم تطبيقها على المحتوى، وليس على
LazyColumn نفسه. في المثال أعلاه، سيضيف العنصر الأول مساحة متروكة 8.dp إلى الجزء العلوي، وسيضيف العنصر الأخير 8.dp إلى الجزء السفلي، وستحتوي جميع العناصر على مساحة متروكة 16.dp على اليسار واليمين.
كمثال آخر، يمكنك تمرير PaddingValues من Scaffold إلى contentPadding من LazyColumn. راجِع دليل العرض من الحافة إلى الحافة.
تباعد المحتوى
لإضافة مسافة بين العناصر، يمكنك استخدام
Arrangement.spacedBy().
يضيف المثال أدناه مسافة 4.dp بين كل عنصر:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
وبالمثل، بالنسبة إلى LazyRow:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
ومع ذلك، تقبل الشبكات كلاً من الترتيبات الرأسية والأفقية:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
مفاتيح العناصر
بشكل تلقائي، يتم ربط حالة كل عنصر بموضعه في القائمة أو الشبكة. ومع ذلك، يمكن أن يتسبب ذلك في حدوث مشكلات إذا تغيرت مجموعة البيانات، لأن
العناصر التي تغير موضعها تفقد فعليًا أي حالة تم تذكرها. إذا تخيلت سيناريو LazyRow داخل LazyColumn، إذا غيّر الصف موضع العنصر، سيفقد المستخدم موضع التمرير داخل الصف.
لمعالجة هذه المشكلة، يمكنك تقديم مفتاح ثابت وفريد لكل عنصر، من خلال توفير
مجموعة مخصّصة للمَعلمة key. يؤدي توفير مفتاح ثابت إلى جعل حالة العنصر
متسقة مع تغييرات مجموعة البيانات:
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
من خلال توفير المفاتيح، يمكنك مساعدة Compose في التعامل مع عمليات إعادة الترتيب بشكل صحيح. على سبيل المثال، إذا كان العنصر يحتوي على حالة محفوظة، سيسمح ضبط المفاتيح لـ Compose بنقل هذه الحالة مع العنصر عند تغيير موضعه.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
ومع ذلك، هناك قيد واحد على الأنواع التي يمكنك استخدامها كمفاتيح للعناصر.
يجب أن يكون نوع المفتاح متوافقًا مع
Bundle، وهي آلية Android للحفاظ على
الحالات عند إعادة إنشاء النشاط. يتيح Bundle استخدام أنواع مثل الأنواع الأساسية
أو التعدادات أو Parcelables.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
يجب أن يكون المفتاح متوافقًا مع Bundle حتى يمكن استعادة rememberSaveable داخل
الدالة المركّبة للعنصر عند إعادة إنشاء النشاط، أو حتى
عند الانتقال بعيدًا عن هذا العنصر ثم العودة إليه.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
الصور المتحركة للعناصر
إذا كنت قد استخدمت تطبيق RecyclerView المصغّر، ستعرف أنّه يحرّك تغييرات العناصر تلقائيًا.
وتوفّر التنسيقات الكسولة الوظيفة نفسها لإعادة ترتيب العناصر.
واجهة برمجة التطبيقات بسيطة، ما عليك سوى ضبط المُعدِّل
animateItem
على محتوى العنصر:
LazyColumn { // It is important to provide a key to each item to ensure animateItem() works as expected. items(books, key = { it.id }) { Row(Modifier.animateItem()) { // ... } } }
يمكنك أيضًا تقديم مواصفات مخصّصة للحركة، إذا لزم الأمر:
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItem( fadeInSpec = tween(durationMillis = 250), fadeOutSpec = tween(durationMillis = 100), placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) ) ) { // ... } } }
تأكَّد من توفير مفاتيح للعناصر حتى تتمكّن من العثور على الموضع الجديد للعنصر الذي تم نقله.
مثال: إضافة تأثيرات حركية إلى العناصر في القوائم الكسولة
باستخدام Compose، يمكنك إضافة تأثيرات حركية إلى التغييرات في العناصر في القوائم الكسولة. عند استخدامها معًا، تنفّذ المقتطفات التالية الصور المتحركة عند إضافة عناصر القائمة الكسولة وإزالتها وإعادة ترتيبها.
يعرض هذا المقتطف قائمة بالسلاسل مع انتقالات متحركة عند إضافة العناصر أو إزالتها أو إعادة ترتيبها:
@Composable fun ListAnimatedItems( items: List<String>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // Use a unique key per item, so that animations work as expected. items(items, key = { it }) { ListItem( headlineContent = { Text(it) }, modifier = Modifier .animateItem( // Optionally add custom animation specs ) .fillParentMaxWidth() .padding(horizontal = 8.dp, vertical = 0.dp), ) } } }
نقاط رئيسية حول الرمز
- تعرض السمة
ListAnimatedItemsقائمة بالسلاسل فيLazyColumnمع انتقالات متحركة عند تعديل العناصر. - تُعيّن الدالة
itemsمفتاحًا فريدًا لكل عنصر في القائمة. يستخدم Compose المفاتيح لتتبُّع العناصر وتحديد التغييرات في مواضعها. - يحدّد
ListItemتنسيق كل عنصر في القائمة. وتتطلّب هذه السمة استخدام المَعلمةheadlineContentالتي تحدّد المحتوى الرئيسي للعنصر. - يطبّق المُعدِّل
animateItemحركات تلقائية على عمليات إضافة العناصر وإزالتها ونقلها.
يعرض المقتطف التالي شاشة تتضمّن عناصر تحكّم لإضافة العناصر وإزالتها، بالإضافة إلى ترتيب قائمة محدّدة مسبقًا:
@Composable private fun ListAnimatedItemsExample( data: List<String>, modifier: Modifier = Modifier, onAddItem: () -> Unit = {}, onRemoveItem: () -> Unit = {}, resetOrder: () -> Unit = {}, onSortAlphabetically: () -> Unit = {}, onSortByLength: () -> Unit = {}, ) { val canAddItem = data.size < 10 val canRemoveItem = data.isNotEmpty() Scaffold(modifier) { paddingValues -> Column( modifier = Modifier .padding(paddingValues) .fillMaxSize() ) { // Buttons that change the value of displayedItems. AddRemoveButtons(canAddItem, canRemoveItem, onAddItem, onRemoveItem) OrderButtons(resetOrder, onSortAlphabetically, onSortByLength) // List that displays the values of displayedItems. ListAnimatedItems(data) } } }
نقاط رئيسية حول الرمز
- يعرض
ListAnimatedItemsExampleشاشة تتضمّن عناصر تحكّم لإضافة العناصر وإزالتها وترتيبها.-
onAddItemوonRemoveItemهما تعبيران lambda يتم تمريرهما إلىAddRemoveButtonsلإضافة عناصر إلى القائمة وإزالتها منها. -
resetOrderوonSortAlphabeticallyوonSortByLengthهي تعبيرات lambda يتم تمريرها إلىOrderButtonsلتغيير ترتيب العناصر في القائمة.
-
- يعرض "
AddRemoveButtons" الزرَّين "إضافة" و"إزالة". ويتيح تفعيل/إيقاف الأزرار ومعالجة النقرات عليها. - يعرض
OrderButtonsأزرار إعادة ترتيب القائمة. وتتلقّى دوال lambda لإعادة ضبط الترتيب وفرز القائمة حسب الطول أو أبجديًا. - تستدعي الدالة
ListAnimatedItemsالدالة المركّبةListAnimatedItems، وتمرّر قائمةdataلعرض قائمة السلاسل المتحركة. تم تحديدdataفي مكان آخر.
ينشئ هذا المقتطف واجهة مستخدم تحتوي على الزرَّين إضافة عنصر وحذف عنصر:
@Composable private fun AddRemoveButtons( canAddItem: Boolean, canRemoveItem: Boolean, onAddItem: () -> Unit, onRemoveItem: () -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { Button(enabled = canAddItem, onClick = onAddItem) { Text("Add Item") } Spacer(modifier = Modifier.padding(25.dp)) Button(enabled = canRemoveItem, onClick = onRemoveItem) { Text("Delete Item") } } }
نقاط رئيسية حول الرمز
- يعرض
AddRemoveButtonsصفًا من الأزرار لإجراء عمليات الإضافة والإزالة في القائمة. - تتحكّم المَعلمتان
canAddItemوcanRemoveItemفي حالة تفعيل الأزرار. إذا كانت قيمةcanAddItemأوcanRemoveItemهي "خطأ"، يتم إيقاف الزر المقابل. - المَعلمتان
onAddItemوonRemoveItemهما دالتا lambda يتم تنفيذهما عندما ينقر المستخدم على الزر المقابل.
أخيرًا، يعرض هذا المقتطف ثلاثة أزرار لترتيب القائمة (إعادة الضبط و الترتيب الأبجدي والطول):
@Composable private fun OrderButtons( resetOrder: () -> Unit, orderAlphabetically: () -> Unit, orderByLength: () -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { var selectedIndex by remember { mutableIntStateOf(0) } val options = listOf("Reset", "Alphabetical", "Length") SingleChoiceSegmentedButtonRow { options.forEachIndexed { index, label -> SegmentedButton( shape = SegmentedButtonDefaults.itemShape( index = index, count = options.size ), onClick = { Log.d("AnimatedOrderedList", "selectedIndex: $selectedIndex") selectedIndex = index when (options[selectedIndex]) { "Reset" -> resetOrder() "Alphabetical" -> orderAlphabetically() "Length" -> orderByLength() } }, selected = index == selectedIndex ) { Text(label) } } } } }
نقاط رئيسية حول الرمز
- يعرض "
OrderButtons"SingleChoiceSegmentedButtonRowللسماح للمستخدمين باختيار طريقة ترتيب القائمة أو إعادة ضبط ترتيبها. يتيح لك مكوّنSegmentedButtonتحديد خيار واحد من قائمة خيارات. -
resetOrderوorderAlphabeticallyوorderByLengthهي دوال lambda يتم تنفيذها عند اختيار الزر المقابل. - يتتبّع متغيّر الحالة
selectedIndexالخيار المحدّد.
النتيجة
يعرض هذا الفيديو نتيجة المقتطفات السابقة عند إعادة ترتيب العناصر:
رؤوس صفحات ثابتة
يكون نمط "العنوان الثابت" مفيدًا عند عرض قوائم البيانات المجمّعة. في ما يلي مثال على "قائمة جهات الاتصال"، مجمّعة حسب الحرف الأول من اسم كل جهة اتصال:

لإنشاء رأس صفحة ثابت باستخدام LazyColumn، يمكنك استخدام
stickyHeader()
، وتوفير محتوى الرأس:
@Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
لإنشاء قائمة تحتوي على عناوين متعددة، مثل مثال "قائمة جهات الاتصال" أعلاه، يمكنك القيام بما يلي:
// This ideally would be done in the ViewModel val grouped = contacts.groupBy { it.firstName[0] } @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
التفاعل مع موضع الانتقال
تحتاج العديد من التطبيقات إلى التفاعل مع التغييرات في موضع التمرير وتنسيق العناصر والاستجابة لها.
تتيح مكوّنات Lazy حالة الاستخدام هذه من خلال رفع
LazyListState:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
في حالات الاستخدام البسيطة، تحتاج التطبيقات عادةً إلى معرفة معلومات عن العنصر الأول المرئي فقط. لهذا الغرض، يوفّر العنصر LazyListState السمتَين firstVisibleItemIndex وfirstVisibleItemScrollOffset.
إذا استخدمنا مثال إظهار زر وإخفائه استنادًا إلى ما إذا كان المستخدم قد مرّر بجانب العنصر الأول:
@Composable fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
تكون قراءة الحالة مباشرةً في التركيب مفيدة عندما تحتاج إلى تعديل
دوال مركّبة أخرى لواجهة المستخدم، ولكن هناك أيضًا سيناريوهات لا تحتاج فيها إلى
معالجة الحدث في التركيب نفسه. ومن الأمثلة الشائعة على ذلك إرسال حدث إحصاءات بعد أن يتجاوز المستخدم نقطة معيّنة. للتعامل مع هذا الأمر بكفاءة، يمكننا استخدام snapshotFlow():
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
يوفّر LazyListState أيضًا معلومات عن جميع العناصر التي يتم عرضها حاليًا وحدودها على الشاشة، من خلال السمة layoutInfo. لمزيد من المعلومات، يمكنك الاطّلاع على فئة
LazyListLayoutInfo.
التحكّم في موضع الانتقال
بالإضافة إلى التفاعل مع موضع الانتقال، من المفيد أيضًا أن تتمكّن التطبيقات من
التحكّم في موضع الانتقال أيضًا.
LazyListState
يتيح ذلك من خلال الدالة scrollToItem()
التي تلتقط موضع التمرير "على الفور"، وanimateScrollToItem()
التي تتيح التمرير باستخدام صورة متحركة (تُعرف أيضًا باسم التمرير السلس):
@Composable fun MessageList(messages: List<Message>) { val listState = rememberLazyListState() // Remember a CoroutineScope to be able to launch val coroutineScope = rememberCoroutineScope() LazyColumn(state = listState) { // ... } ScrollToTopButton( onClick = { coroutineScope.launch { // Animate scroll to the first item listState.animateScrollToItem(index = 0) } } ) }
مجموعات البيانات الكبيرة (تقسيم الصفحات)
تتيح مكتبة Paging للتطبيقات إمكانية عرض قوائم كبيرة من العناصر، وتحميل وعرض أجزاء صغيرة من القائمة حسب الحاجة. توفّر Paging 3.0 والإصدارات الأحدث إمكانية استخدام Compose من خلال مكتبة androidx.paging:paging-compose.
لعرض قائمة بالمحتوى المقسّم إلى صفحات، يمكننا استخدام دالة الإضافة
collectAsLazyPagingItems()
ثم تمرير دالة
LazyPagingItems
التي تم عرضها إلى items() في LazyColumn. على غرار دعم ميزة "تقسيم الصفحات" في طرق العرض، يمكنك
عرض العناصر النائبة أثناء تحميل البيانات من خلال التحقّق مما إذا كان item هو null:
@Composable fun MessageList(pager: Pager<Int, Message>) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it.id } ) { index -> val message = lazyPagingItems[index] if (message != null) { MessageRow(message) } else { MessagePlaceholder() } } } }
نصائح حول استخدام التنسيقات الكسولة
هناك بعض النصائح التي يمكنك أخذها في الاعتبار لضمان عمل التنسيقات الكسولة على النحو المنشود.
تجنُّب استخدام عناصر بحجم 0 بكسل
يمكن أن يحدث ذلك في سيناريوهات تتوقّع فيها، على سبيل المثال، استرداد بعض البيانات بشكل غير متزامن، مثل الصور، لملء عناصر القائمة في مرحلة لاحقة. سيؤدي ذلك إلى إنشاء جميع عناصر التنسيق الكسول في القياس الأول، لأنّ ارتفاعها هو 0 بكسل ويمكن أن تتسع جميعها في إطار العرض. بعد تحميل العناصر وتوسيع ارتفاعها، ستتجاهل التنسيقات الكسولة جميع العناصر الأخرى التي تم إنشاؤها بدون داعٍ في المرة الأولى لأنّها لا تتناسب مع إطار العرض. لتجنُّب ذلك، عليك ضبط الحجم التلقائي للعناصر، حتى يتمكّن التنسيق الكسول من إجراء الحساب الصحيح لعدد العناصر التي يمكن أن تتسع في إطار العرض:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
عندما تعرف الحجم التقريبي للعناصر بعد تحميل البيانات بشكل غير متزامن، من الأفضل التأكّد من بقاء حجم العناصر كما هو قبل التحميل وبعده، على سبيل المثال، من خلال إضافة بعض العناصر النائبة. سيساعد ذلك في الحفاظ على موضع الانتقال الصحيح.
تجنَّب تداخل المكوّنات القابلة للتمرير في الاتجاه نفسه
ينطبق ذلك فقط على الحالات التي يتم فيها دمج عناصر ثانوية قابلة للتمرير بدون حجم محدّد مسبقًا داخل عنصر رئيسي آخر قابل للتمرير في الاتجاه نفسه. على سبيل المثال، محاولة دمج عنصر فرعي LazyColumn بدون ارتفاع ثابت داخل عنصر رئيسي Column قابل للتمرير عموديًا:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
بدلاً من ذلك، يمكن تحقيق النتيجة نفسها من خلال تضمين جميع الدوال المركّبة
داخل دالة رئيسية واحدة LazyColumn واستخدام لغة DSL الخاصة بها لتمرير أنواع مختلفة من
المحتوى. يتيح ذلك إرسال عناصر فردية، بالإضافة إلى عناصر قائمة متعددة، كل ذلك في مكان واحد:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
يُرجى العِلم أنّه يُسمح بالحالات التي يتم فيها دمج تنسيقات اتجاهات مختلفة،
على سبيل المثال، عنصر رئيسي قابل للتمرير Row وعنصر فرعي LazyColumn:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
بالإضافة إلى الحالات التي لا تزال تستخدم فيها تخطيطات الاتجاه نفسها، ولكنك تقوم أيضًا بتعيين حجم ثابت للعناصر الفرعية المتداخلة:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
احذر من وضع عناصر متعددة في عنصر واحد
في هذا المثال، تُصدر دالة lambda للعنصر الثاني عنصرَين في مجموعة واحدة:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
ستتعامل التنسيقات الكسولة مع هذا الأمر على النحو المتوقع، حيث ستضع العناصر واحدًا تلو الآخر كما لو كانت عناصر مختلفة. ومع ذلك، هناك بعض المشكلات في القيام بذلك.
عندما يتم إصدار عناصر متعددة كجزء من عنصر واحد، يتم التعامل معها ككيان واحد، ما يعني أنه لا يمكن تكوينها بشكل فردي بعد الآن. إذا أصبح عنصر واحد مرئيًا على الشاشة، يجب إنشاء جميع العناصر المقابلة للعنصر وقياسها. ويمكن أن يؤدي ذلك إلى الإضرار بالأداء إذا تم استخدامه
بشكل مفرط. في الحالة القصوى المتمثلة في وضع جميع العناصر في عنصر واحد، فإن ذلك
يهزم تمامًا الغرض من استخدام التخطيطات الكسولة. بالإضافة إلى المشاكل المحتملة في الأداء، فإنّ وضع المزيد من العناصر في عنصر واحد سيؤدي أيضًا إلى تداخل مع scrollToItem() وanimateScrollToItem().
ومع ذلك، هناك حالات استخدام صالحة لوضع عناصر متعددة في عنصر واحد، مثل وجود فواصل داخل القائمة. يجب ألا تؤدي الفواصل إلى تغيير فهارس التمرير، لأنّها لا تُعتبر عناصر مستقلة. بالإضافة إلى ذلك، لا يتأثر الأداء لأنّ الفواصل صغيرة. من المحتمل أن يكون الفاصل مرئيًا عندما يكون العنصر الذي يسبقه مرئيًا، لذا يمكن أن يكون جزءًا من العنصر السابق:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
استخدام الترتيبات المخصّصة
تحتوي القوائم الكسولة عادةً على العديد من العناصر، وتشغل مساحة أكبر من حجم حاوية الانتقال. ومع ذلك، عندما يتم ملء قائمتك ببعض العناصر، يمكن أن يكون لتصميمك متطلبات أكثر تحديدًا لكيفية وضعها في إطار العرض.
لتحقيق ذلك، يمكنك استخدام سمة مخصّصة لمجال معيّن
Arrangement
وتمريرها إلى LazyColumn. في المثال التالي، يحتاج الكائن TopWithFooter فقط إلى تنفيذ الطريقة arrange. أولاً، سيتم وضع العناصر واحدًا تلو الآخر. ثانيًا، إذا كان إجمالي الارتفاع المستخدَم أقل من ارتفاع إطار العرض، سيتم وضع التذييل في الأسفل:
object TopWithFooter : Arrangement.Vertical { override fun Density.arrange( totalSize: Int, sizes: IntArray, outPositions: IntArray ) { var y = 0 sizes.forEachIndexed { index, size -> outPositions[index] = y y += size } if (y < totalSize) { val lastIndex = outPositions.lastIndex outPositions[lastIndex] = totalSize - sizes.last() } } }
ننصحك بإضافة contentType
بدءًا من الإصدار 1.2 من Compose، لتحقيق أفضل أداء لتخطيط Lazy، ننصحك بإضافة
contentType
إلى القوائم أو الشبكات. يتيح لك ذلك تحديد نوع المحتوى لكل عنصر في التنسيق، في الحالات التي تقوم فيها بإنشاء قائمة أو شبكة تتكون من عدة أنواع مختلفة من العناصر:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
عند تقديم
contentType،
يمكن لـ Compose إعادة استخدام التركيبات فقط
بين العناصر من النوع نفسه. بما أنّ إعادة الاستخدام تكون أكثر كفاءة عند إنشاء عناصر ذات بنية مشابهة، فإنّ توفير أنواع المحتوى يضمن عدم محاولة Compose إنشاء عنصر من النوع "أ" فوق عنصر مختلف تمامًا من النوع "ب". يساعد ذلك في زيادة فوائد إعادة استخدام
التركيب وأداء التنسيق الكسول إلى أقصى حد.
قياس الأداء
يمكنك قياس أداء التنسيق الكسول بشكل موثوق فقط عند تشغيله في وضع الإصدار ومع تفعيل تحسين R8. في الإصدارات المخصّصة لتصحيح الأخطاء، قد تظهر عملية الانتقال في التنسيق الكسول أبطأ. لمزيد من المعلومات حول هذا الموضوع، يُرجى الاطّلاع على مقالة أداء Compose.
مراجع إضافية
- إنشاء قائمة محدودة قابلة للتمرير
- إنشاء شبكة قابلة للتمرير
- عرض عناصر التنقّل المُدمَجة في قائمة
- فلترة قائمة أثناء الكتابة
- التحميل الكسول للبيانات باستخدام القوائم ومكتبة Paging
- إنشاء قائمة باستخدام أنواع عناصر متعددة
- فيديو: القوائم في Compose
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- نقل
RecyclerViewإلى Lazy list - حفظ حالة واجهة المستخدم في Compose
- Kotlin لـ Jetpack Compose