تحتاج العديد من التطبيقات إلى عرض مجموعات من العناصر. يوضّح هذا المستند كيفية تنفيذ ذلك بكفاءة في 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
المربع لغة خاصة بالمجال تتيح للتطبيقات وصف محتوى السلعة. بعد ذلك، يصبح المكوّن
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
إمكانية عرض العناصر في شبكة. تعرض شبكة Lazy العمودية عناصرها في حاوية يمكن تصفّحها عموديًا، وتمتد على عدة أعمدة، بينما تتصرف شبكات Lazy الأفقية بالطريقة نفسها على المحور الأفقي.
تتضمّن الشبكات إمكانات واجهة برمجة التطبيقات الفعّالة نفسها التي تتضمّنها القوائم، كما أنّها تستخدم لغة خاصة بالمجال مشابهة جدًا، وهي 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") } // ... }
Lazy staggered grid
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
مع أنواع مثل الأنواع الأساسية أو التعدادات أو Parcelable.
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
هما تعبيران لامدا يتم تمريرهما إلى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
هما دالتان لامدا يتم تنفيذهما عندما ينقر المستخدم على الزر المعنيّ.
أخيرًا، يعرض هذا المقتطف ثلاثة أزرار لترتيب القائمة (إعادة التعيين وأبجديًا والمدة):
@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
للسماح للمستخدمين باختيار طريقة ترتيب في القائمة أو إعادة ضبط ترتيب القائمة. يتيح لك العنصر ASegmentedButton
تحديد خيار واحد من قائمة خيارات. -
resetOrder
وorderAlphabetically
وorderByLength
هي دوال lambda يتم تنفيذها عند تحديد الزر المقابل. - يتتبّع متغيّر الحالة
selectedIndex
الخيار المحدّد.
النتيجة
يعرض هذا الفيديو نتيجة المقتطفات السابقة عند إعادة ترتيب العناصر:
العناوين الثابتة (تجريبية)
يكون نمط "العنوان الثابت" مفيدًا عند عرض قوائم بالبيانات المجمّعة. في ما يلي مثال على "قائمة جهات الاتصال"، وهي مقسّمة حسب الحرف الأول من اسم كل جهة اتصال:
لإنشاء رأس ثابت باستخدام LazyColumn
، يمكنك استخدام الدالة التجريبية
stickyHeader()
مع توفير محتوى الرأس:
@OptIn(ExperimentalFoundationApi::class) @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] } @OptIn(ExperimentalFoundationApi::class) @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 للتطبيقات إمكانية
التعامل مع القوائم الكبيرة التي تتضمّن عناصر كثيرة، وذلك من خلال تحميل أجزاء صغيرة من القائمة وعرضها عند
الضرورة. توفّر مكتبة
androidx.paging:paging-compose
إمكانية استخدام الإصدار 3.0 من 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 بكسل
يمكن أن يحدث ذلك في سيناريوهات تتوقّع فيها، على سبيل المثال، استرداد بعض البيانات بشكل غير متزامن، مثل الصور، لملء عناصر القائمة في مرحلة لاحقة. سيؤدي ذلك إلى أن يركّب التنسيق Lazy جميع عناصره في القياس الأول، لأنّ ارتفاعها يبلغ 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) } // ... }
ستتعامل التخطيطات الكسولة مع هذا الأمر على النحو المتوقّع، إذ ستعرض العناصر واحدًا تلو الآخر كما لو كانت عناصر مختلفة. ومع ذلك، هناك مشكلتان في ذلك.
عندما يتم إصدار عناصر متعددة كجزء من عنصر واحد، يتم التعامل معها ككيان واحد، ما يعني أنّه لا يمكن إنشاءها بشكل فردي بعد ذلك. إذا أصبح أحد العناصر مرئيًا على الشاشة، يجب إنشاء جميع العناصر المقابلة للعنصر وقياسها. وقد يؤدي ذلك إلى الإضرار بالأداء إذا تم استخدامه بشكل مفرط. في الحالة القصوى المتمثلة في وضع جميع العناصر في عنصر واحد، يؤدي ذلك إلى إبطال الغرض من استخدام تنسيقات Lazy تمامًا. بالإضافة إلى المشاكل المحتملة في الأداء، سيؤدي وضع المزيد من العناصر في منتج واحد إلى التأثير سلبًا في 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
بدءًا من Compose 1.2، لتحقيق أقصى أداء في Lazy
layout، ننصحك بإضافة
contentType
إلى القوائم أو الجداول. يتيح لك ذلك تحديد نوع المحتوى لكل عنصر من عناصر التصميم، في الحالات التي تنشئ فيها قائمة أو شبكة تتألف من أنواع مختلفة من العناصر:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
عند تقديم
contentType
،
يمكن أن يعيد Compose استخدام التراكيب
بين العناصر من النوع نفسه فقط. بما أنّ إعادة الاستخدام تكون أكثر فعالية عند إنشاء عناصر ذات بنية مشابهة، يضمن توفير أنواع المحتوى عدم محاولة Compose إنشاء عنصر من النوع A فوق عنصر مختلف تمامًا من النوع B. يساعد ذلك في تحقيق أقصى استفادة من إعادة استخدام التركيب وتحسين أداء التنسيق Lazy.
قياس الأداء
لا يمكنك قياس أداء التنسيق الكسول بشكل موثوق إلا عند تشغيله في وضع الإصدار مع تفعيل تحسين R8. في إصدارات تصحيح الأخطاء، قد يبدو التمرير في Lazy layout أبطأ. لمزيد من المعلومات حول هذا الموضوع، يمكنك الاطّلاع على أداء Compose.
مراجع إضافية
- إنشاء قائمة قابلة للتمرير بشكل نهائي
- إنشاء شبكة قابلة للتمرير
- عرض عناصر التمرير المتداخل في قائمة
- فلترة قائمة أثناء الكتابة
- تحميل البيانات بشكل غير متزامن باستخدام القوائم والتصفُّح على عدّة صفحات
- إنشاء قائمة باستخدام أنواع عناصر متعددة
- الفيديو: القوائم في وضع الإنشاء
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- نقل
RecyclerView
إلى Lazy list - حفظ حالة واجهة المستخدم في Compose
- Kotlin لـ Jetpack Compose