القوائم والشبكات

تحتاج العديد من التطبيقات إلى عرض مجموعات من العناصر. يشرح هذا المستند كيفية تنفيذ ذلك بفعالية في Jetpack Compose.

إذا كنت تعلم أنّ حالة الاستخدام لا تتطلّب الانتقال إلى أعلى أو أسفل الصفحة، يمكنك استخدام Column أو Row (حسب الاتجاه) وعرض محتوى كل عنصر من خلال التنقّل في قائمة بالطريقة التالية:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

يمكننا إتاحة التنقّل في Column باستخدام مفتاح التعديل verticalScroll().

القوائم الكسولة

إذا كنت بحاجة إلى عرض عدد كبير من العناصر (أو قائمة ذات طول غير معروف)، قد يؤدي استخدام تنسيق مثل Column إلى حدوث مشاكل في الأداء، لأنّه سيتم إنشاء جميع العناصر وتحديد ما إذا كانت مرئية أم لا.

توفّر أداة "الإنشاء" مجموعة من المكوّنات التي تُنشئ وتُرتّب العناصر التي تكون مرئية في إطار عرض المكوّن فقط. وتشمل هذه المكوّنات LazyColumn وLazyRow.

كما يشير الاسم، الفرق بين LazyColumn وLazyRow هو الاتجاه الذي يتم فيه عرض العناصر والانتقال إلى الأسفل. يعرض الرمز LazyColumn قائمة قابلة للانتقال عموديًا، ويعرض الرمز LazyRowقائمة قابلة للانتقال أفقيًا.

تختلف المكونات غير المُشغَّلة عن معظم التنسيقات في ميزة "الإنشاء". وبدلاً من قبول مَعلمة حظر المحتوى @Composable والسماح للتطبيقات بإصدار عناصر قابلة للإنشاء مباشرةً، توفّر "المكوّنات الكسولة" حظرًا LazyListScope.(). يوفّر هذا العنصر LazyListScope لغة وصفية تتيح للتطبيقات وصف محتوى العناصر. يصبح العنصر الكسول مسؤولاً بعد ذلك عن إضافة محتوى كل عنصر حسبما يتطلبه التنسيق وموضع التمرير.

خط DSL واحد (LazyListScope)

يوفر 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()
)

الشكل 1. مثال على الشبكة العمودية الكسولة المرحة

لضبط عدد ثابت من الأعمدة، يمكنك استخدام 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()
)

شبكة صور مرتَّبة كسولة في Compose
الشكل 2. مثال على شبكة عمودية كسولة متدرجة مع أعمدة ثابتة

تعبئة المحتوى

في بعض الأحيان، ستحتاج إلى إضافة مسافة حول حواف المحتوى. وتسمح لك المكوّنات الكسولة بتمرير بعض من PaddingValues إلى مَعلمة contentPadding لإتاحة ما يلي:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

في هذا المثال، نضيف 16.dp من المسافة البادئة إلى الحواف الأفقية (اليسار واليمين)، ثم 8.dp إلى أعلى المحتوى وأسفله.

ملاحظة: يتم تطبيق هذه المساحة المتروكة على المحتوى، وليس على LazyColumn نفسه. في المثال أعلاه، سيضيف العنصر الأول مساحة متروكة 8.dp في الأعلى، وسيضيف العنصر الأخير 8.dp إلى أسفله، وستحتوي جميع العناصر على مساحة متروكة 16.dp على اليمين واليسار.

تباعد المحتوى

لإضافة مسافات بين العناصر، يمكنك استخدام 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)
    }
}

من خلال تقديم مفاتيح، يمكنك مساعدة ميزة "الإنشاء" في معالجة عمليات إعادة الترتيب بشكل صحيح. على سبيل المثال، إذا كان العنصر يحتوي على حالة محفوظة، ستسمح مفاتيح الإعدادات لميزة الإنشاء بنقل هذه الحالة مع العنصر عند تغيير موضعه.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

ومع ذلك، هناك قيد واحد على الأنواع التي يمكنك استخدامها كمفاتيح عناصر. يجب أن يكون نوع المفتاح متوافقًا مع Bundle، وهي آلية Android للحفاظ على الحالات عند إعادة إنشاء النشاط. يتوافق Bundle مع أنواع مثل العناصر الأولية أو التعدادات أو قطع الأراضي.

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)
            )
        ) {
            // ...
        }
    }
}

تأكد من توفير مفاتيح للعناصر التي تم نقلها ليكون من الممكن العثور على الموضع الجديد للعنصر الذي تم نقله.

العناوين الثابتة (ميزة تجريبية)

يكون نمط "العنوان الثابت" مفيدًا عند عرض قوائم بالبيانات المجمّعة. يمكنك الاطلاع أدناه على مثال لـ "قائمة جهات الاتصال"، مجمعة حسب الحرف الأول من كل جهة اتصال:

فيديو لهاتف يتنقّل للأعلى وللأسفل في قائمة جهات اتصال

لإنشاء عنوان ثابت باستخدام 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)
            }
        }
    }
}

التفاعل مع موضع التمرير

تحتاج العديد من التطبيقات إلى الاستجابة والاستماع إلى تغييرات موضع التمرير وتنسيق العناصر. تتوافق المكوّنات الكسولة مع حالة الاستخدام هذه من خلال رفع 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)
            }
        }
    )
}

مجموعات البيانات الكبيرة (الفهرسة)

تتيح مكتبة الفهرسة للتطبيقات استخدام قوائم كبيرة من العناصر، وتحميل أجزاء صغيرة من القائمة وعرضها عند الضرورة. توفّر ميزة "التنقّل بين الصفحات" 3.0 والإصدارات الأحدث إمكانية استخدام ميزة "الإنشاء" من خلال مكتبة 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.

لتحسين أداء "التنسيق الكسول" بدءًا من Compose 1.2، ننصحك بإضافة السمة contentType إلى القوائم أو الشبكات. يتيح لك هذا تحديد نوع المحتوى لكل عنصر من عناصر التخطيط، في الحالات التي تنشئ فيها قائمة أو شبكة تتكون من أنواع مختلفة من العناصر:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

عند تقديم contentType، يمكن لأداة "الإنشاء" إعادة استخدام التركيبات فقط بين العناصر من النوع نفسه. بما أن إعادة الاستخدام تكون أكثر فعالية عند إنشاء عناصر ذات بنية متشابهة، فإن توفير أنواع المحتوى يضمن ألا يحاول تطبيق "إنشاء" إنشاء عنصر من النوع A بالإضافة إلى عنصر مختلف تمامًا من النوع "ب". يساعد ذلك في زيادة مزايا إعادة استخدام العناصر المكوّنة وتحسين أداء التنسيق المُتأخّر.

قياس الأداء

لا يمكنك قياس أداء التنسيق المُتأخّر إلا بشكل موثوق عند التشغيل في وضع الإصدار وتفعيل تحسين R8. في عمليات تصحيح الأخطاء، قد يبدو التنقّل في ميزة "العرض البطيء" أبطأ. لمزيد من المعلومات حول هذا الموضوع، يُرجى الاطّلاع على مقالة أداء ميزة "الإنشاء".