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

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

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

النتيجة

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

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

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

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

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

لإنشاء رأس ثابت باستخدام 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. على غرار ميزة &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 بكسل

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

مراجع إضافية