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

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

الشكل 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()
)
شبكة صور متداخلة يتم تحميلها بأسلوب &quot;التحميل الكسول&quot; في 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 استخدام أنواع مثل الأنواع الأساسية أو التعدادات أو 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 الخيار المحدّد.

النتيجة

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

الشكل 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)
            }
        }
    )
}

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

تتيح مكتبة 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.

مراجع إضافية