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

تحتاج العديد من التطبيقات إلى عرض مجموعات من العناصر، ويوضّح هذا المستند كيفية تنفيذ ذلك بكفاءة في 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 vertical grid العناصر في حاوية قابلة للتمرير عموديًا، وتمتد على أعمدة متعددة، بينما تتشابه سلوكيات Lazy horizontal grids على المحور الأفقي.

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

كمثال آخر، يمكنك تمرير 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 للسماح للمستخدمين بتحديد طريقة ترتيب في القائمة أو إعادة ضبط ترتيب القائمة. يتيح لك المكوّن SegmentedButton تحديد خيار واحد من قائمة الخيارات.
  • resetOrder وorderAlphabetically وorderByLength هي دوال lambda يتم تنفيذها عند تحديد الزر المقابل.
  • يتتبّع متغيّر الحالة selectedIndex الخيار المحدّد.

النتيجة

يعرض هذا الفيديو نتيجة المقتطفات السابقة عند إعادة ترتيب العناصر:

الشكل 1: قائمة تحرّك انتقالات العناصر عند إضافة عناصر أو إزالتها أو ترتيبها.

العناوين الثابتة

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

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

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

مجموعات البيانات الكبيرة (تقسيم إلى صفحات)

تتيح مكتبة التقسيم إلى صفحات للتطبيقات إمكانية عرض قوائم كبيرة من العناصر، وتحميل أجزاء صغيرة من القائمة وعرضها عند الحاجة. وتوفّر الإصدارات 3.0 والإصدارات الأحدث من مكتبة التقسيم إلى صفحات إمكانية استخدام Compose من خلال مكتبة androidx.paging:paging-compose.

لعرض قائمة بالمحتوى المقسّم إلى صفحات، يمكننا استخدام دالة الإضافة collectAsLazyPagingItems() ، ثم تمرير LazyPagingItems الذي تم عرضه إلى items() في LazyColumn. على غرار ميزة &quot;تقسيم المحتوى إلى صفحات&quot; في طرق العرض، يمكنك عرض عناصر نائبة أثناء تحميل البيانات من خلال التحقّق مما إذا كانت 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) }
    // ...
}

ستتعامل التنسيقات الكسولة مع هذا الأمر على النحو المتوقّع، إذ ستعرض العناصر واحدًا تلو الآخر كما لو كانت عناصر مختلفة. ومع ذلك، هناك مشكلتان في ذلك.

عندما يتم عرض عناصر متعدّدة كجزء من عنصر واحد، يتم التعامل معها ككيان واحد، ما يعني أنّه لا يمكن إنشاءها بشكل فردي بعد ذلك. وإذا ظهر عنصر واحد على الشاشة، يجب إنشاء جميع العناصر المقابلة للعنصر وقياسها، ما قد يؤدي إلى انخفاض الأداء في حال الاستخدام المفرط. وفي الحالة القصوى المتمثلة في وضع جميع العناصر في عنصر واحد، يتم إبطال الغرض من استخدام Lazy layouts تمامًا. وإلى جانب المشاكل المحتملة في الأداء، سيؤدي وضع المزيد من العناصر في عنصر واحد أيضًا إلى حدوث تداخل مع 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 Layout.

قياس الأداء

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

مراجع إضافية