लेज़ी लिस्ट और लेज़ी ग्रिड

कई ऐप्लिकेशन को आइटम के कलेक्शन दिखाने होते हैं. इस दस्तावेज़ में बताया गया है कि Jetpack Compose में इसे असरदार तरीके से कैसे किया जा सकता है.

अगर आपको पता है कि इस्तेमाल के उदाहरण में स्क्रोल करने की ज़रूरत नहीं है, तो Column या Row (दिशा के हिसाब से) का इस्तेमाल करें. साथ ही, हर आइटम के कॉन्टेंट को इस तरह से दोहराकर दिखाएं:

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

verticalScroll() मॉडिफ़ायर का इस्तेमाल करके, Column को स्क्रोल किया जा सकता है.

लेज़ी लिस्ट

अगर आपको बड़ी संख्या में आइटम (या अनिश्चित लंबाई वाली सूची) दिखानी है, तो Column जैसे लेआउट का इस्तेमाल करने से परफ़ॉर्मेंस से जुड़ी समस्याएं हो सकती हैं. ऐसा इसलिए, क्योंकि सभी आइटम कंपोज़ किए जाएंगे और लेआउट किए जाएंगे, भले ही वे दिखें या न दिखें.

Compose, कॉम्पोनेंट का एक सेट उपलब्ध कराता है. यह सिर्फ़ उन आइटम को कंपोज़ और लेआउट करता है जो कॉम्पोनेंट के व्यूपोर्ट में दिखते हैं. इन कॉम्पोनेंट में LazyColumn और LazyRow शामिल हैं.

नाम से ही पता चलता है कि LazyColumn और LazyRow में अंतर यह है कि वे अपने आइटम किस ओर रखते हैं और स्क्रोल करते हैं. LazyColumn से वर्टिकल स्क्रोलिंग वाली सूची बनती है. वहीं, LazyRow से हॉरिज़ॉन्टल स्क्रोलिंग वाली सूची बनती है.

Lazy कॉम्पोनेंट, Compose में मौजूद ज़्यादातर लेआउट से अलग होते हैं. @Composable कॉन्टेंट ब्लॉक पैरामीटर स्वीकार करने के बजाय, ऐप्लिकेशन को सीधे तौर पर कंपोज़ेबल दिखाने की अनुमति देने वाले लेज़ी कॉम्पोनेंट, LazyListScope.() ब्लॉक उपलब्ध कराते हैं. इस LazyListScope ब्लॉक में एक डीएसएल उपलब्ध है. इसकी मदद से ऐप्लिकेशन, आइटम के कॉन्टेंट के बारे में बता सकते हैं. इसके बाद, Lazy कॉम्पोनेंट की यह ज़िम्मेदारी होती है कि वह लेआउट और स्क्रोल पोज़िशन के हिसाब से, हर आइटम का कॉन्टेंट जोड़े.

LazyListScope डीएसएल

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 कंपोज़ेबल, आइटम को ग्रिड में दिखाने की सुविधा देते हैं. LazyVerticalGrid, अपने आइटम को वर्टिकल तौर पर स्क्रोल किए जा सकने वाले कंटेनर में दिखाएगा. यह कंटेनर, कई कॉलम में फैला होगा. वहीं, LazyHorizontalGrid में हॉरिज़ॉन्टल ऐक्सिस पर यही व्यवहार दिखेगा.

ग्रिड में, लिस्ट की तरह ही एपीआई की बेहतर सुविधाएं होती हैं. साथ ही, ये कॉन्टेंट के बारे में बताने के लिए, LazyGridScope.() जैसे मिलते-जुलते डीएसएल का इस्तेमाल करते हैं.

फ़ोन के स्क्रीनशॉट में, फ़ोटो का ग्रिड दिख रहा है

LazyVerticalGrid में मौजूद columns पैरामीटर और LazyHorizontalGrid में मौजूद rows पैरामीटर से यह कंट्रोल किया जाता है कि सेल को कॉलम या लाइन में कैसे बनाया जाए. इस उदाहरण में, आइटम को ग्रिड में दिखाया गया है. इसमें GridCells.Adaptive का इस्तेमाल करके, हर कॉलम को कम से कम 128.dp चौड़ा सेट किया गया है:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid की मदद से, आइटम की चौड़ाई तय की जा सकती है. इसके बाद, ग्रिड में ज़्यादा से ज़्यादा कॉलम फ़िट हो जाएंगे. कॉलम की संख्या तय होने के बाद, बची हुई चौड़ाई को सभी कॉलम में बराबर बांट दिया जाता है. साइज़ तय करने का यह अडैप्टिव तरीका, खास तौर पर अलग-अलग स्क्रीन साइज़ पर आइटम के सेट दिखाने के लिए काम आता है.

अगर आपको इस्तेमाल किए जाने वाले कॉलम की सही संख्या पता है, तो इसके बजाय, ज़रूरी कॉलम की संख्या वाला GridCells.Fixed का इंस्टेंस दिया जा सकता है.

अगर आपके डिज़ाइन में सिर्फ़ कुछ आइटम के लिए नॉन-स्टैंडर्ड डाइमेंशन की ज़रूरत है, तो आइटम के लिए कस्टम कॉलम स्पैन देने के लिए, ग्रिड सपोर्ट का इस्तेमाल किया जा सकता है. item और items तरीकों के LazyGridScope DSL span पैरामीटर की मदद से, कॉलम स्पैन तय करें. 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()
)

पहली इमेज. लेज़ी स्टैगर्ड वर्टिकल ग्रिड का उदाहरण

कॉलम की संख्या तय करने के लिए, StaggeredGridCells.Adaptive के बजाय StaggeredGridCells.Fixed(columns) का इस्तेमाल किया जा सकता है. इससे उपलब्ध चौड़ाई को कॉलम की संख्या (या हॉरिज़ॉन्टल ग्रिड के लिए पंक्तियों) से भाग दिया जाता है. साथ ही, हर आइटम उस चौड़ाई (या हॉरिज़ॉन्टल ग्रिड के लिए ऊंचाई) को लेता है:

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 में इमेज का लेज़ी स्टैगर्ड ग्रिड
दूसरी इमेज. पहले से तय किए गए कॉलम के साथ लेज़ी स्टैगर्ड वर्टिकल ग्रिड का उदाहरण

कॉन्टेंट पैडिंग

कभी-कभी आपको कॉन्टेंट के किनारों पर पैडिंग जोड़ने की ज़रूरत पड़ सकती है. लेज़ी कंपोनेंट की मदद से, contentPadding पैरामीटर में कुछ PaddingValues पास किए जा सकते हैं, ताकि इस सुविधा का इस्तेमाल किया जा सके:

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

इस उदाहरण में, हमने हॉरिज़ॉन्टल किनारों (बाएं और दाएं) पर 16.dp पैडिंग जोड़ी है. इसके बाद, कॉन्टेंट के ऊपर और नीचे 8.dp पैडिंग जोड़ी है.

कृपया ध्यान दें कि यह पैडिंग, कॉन्टेंट पर लागू होती है, न कि LazyColumn पर. ऊपर दिए गए उदाहरण में, पहले आइटम के ऊपर 8.dp पैडिंग जोड़ी जाएगी, आखिरी आइटम के नीचे 8.dp पैडिंग जोड़ी जाएगी, और सभी आइटम के बाईं और दाईं ओर 16.dp पैडिंग जोड़ी जाएगी.

एक और उदाहरण के तौर पर, Scaffold के PaddingValues को LazyColumn के contentPadding में पास किया जा सकता है. किनारे से किनारे तक गाइड देखें.

कॉन्टेंट के बीच की दूरी

आइटम के बीच स्पेस जोड़ने के लिए, 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)
    }
}

आइटम के पासकोड

डिफ़ॉल्ट रूप से, हर आइटम की स्थिति को सूची या ग्रिड में आइटम की जगह के हिसाब से सेट किया जाता है. हालांकि, डेटासेट में बदलाव होने पर इससे समस्याएं हो सकती हैं, क्योंकि जिन आइटम की पोज़िशन बदल जाती है उनकी याद रखी गई स्थिति मिट जाती है. अगर LazyColumn में LazyRow के इस्तेमाल का उदाहरण लिया जाए, तो लाइन में आइटम की जगह बदलने पर, उपयोगकर्ता को लाइन में अपनी स्क्रोल की गई जगह नहीं दिखेगी.

इस समस्या से बचने के लिए, हर आइटम के लिए एक स्टेबल और यूनीक कुंजी दी जा सकती है. इसके लिए, 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, प्रिमिटिव, एनम या पार्सलेबल जैसे टाइप के साथ काम करता है.

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, लैम्ब्डा एक्सप्रेशन हैं. इन्हें OrderButtons को पास किया जाता है, ताकि सूची में मौजूद आइटम का क्रम बदला जा सके.
  • AddRemoveButtons में "जोड़ें" और "हटाएं" बटन दिखते हैं. यह कुकी, बटन को चालू/बंद करती है और बटन पर होने वाले क्लिक को मैनेज करती है.
  • OrderButtons से, सूची का क्रम बदलने के लिए बटन दिखते हैं. यह फ़ंक्शन, क्रम को रीसेट करने और सूची को लंबाई या वर्णमाला के हिसाब से क्रम में लगाने के लिए, लैम्डा फ़ंक्शन को स्वीकार करता है.
  • 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, लैम्डा फ़ंक्शन हैं. ये फ़ंक्शन, इनसे जुड़े बटन को चुनने पर काम करते हैं.
  • selectedIndex स्टेट वैरिएबल, चुने गए विकल्प को ट्रैक करता है.

नतीजा

इस वीडियो में, आइटम का क्रम बदलने पर पिछले स्निपेट के नतीजे दिखाए गए हैं:

पहली इमेज. यह एक ऐसी सूची होती है जिसमें आइटम जोड़े जाने, हटाए जाने या क्रम से लगाए जाने पर, आइटम ट्रांज़िशन को ऐनिमेट किया जाता है.

स्टिकी हेडर

ग्रुप किए गए डेटा की सूचियां दिखाने के लिए, ‘स्टिक हेडर’ पैटर्न का इस्तेमाल किया जाता है. यहां ‘संपर्कों की सूची’ का एक उदाहरण दिया गया है. इसे हर संपर्क के नाम के पहले अक्षर के हिसाब से ग्रुप किया गया है:

फ़ोन पर, संपर्कों की सूची में ऊपर और नीचे की ओर स्क्रोल करने का वीडियो

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)
            }
        }
    }
}

स्क्रोल की पोज़िशन के हिसाब से बदलाव करना

कई ऐप्लिकेशन को स्क्रोल की पोज़िशन और आइटम लेआउट में होने वाले बदलावों पर प्रतिक्रिया देने और उन्हें सुनने की ज़रूरत होती है. लेज़ी कॉम्पोनेंट, 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()
        }
    }
}

देखें.

कंपोज़िशन में सीधे तौर पर स्थिति को पढ़ना तब काम आता है, जब आपको अन्य यूज़र इंटरफ़ेस (यूआई) कंपोज़ेबल अपडेट करने हों. हालांकि, ऐसे भी मामले होते हैं जहां इवेंट को उसी कंपोज़िशन में हैंडल करने की ज़रूरत नहीं होती. इसका एक सामान्य उदाहरण यह है कि जब उपयोगकर्ता किसी तय पॉइंट से आगे स्क्रोल कर लेता है, तब Analytics इवेंट भेजना. इस समस्या को आसानी से हल करने के लिए, हम 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 3.0 और इसके बाद के वर्शन में, androidx.paging:paging-compose लाइब्रेरी की मदद से Compose का इस्तेमाल किया जा सकता है.

पेज वाले कॉन्टेंट की सूची दिखाने के लिए, हम collectAsLazyPagingItems() एक्सटेंशन फ़ंक्शन का इस्तेमाल कर सकते हैं. इसके बाद, LazyColumn में items() को मिले LazyPagingItems को पास कर सकते हैं. व्यू में पेजिंग की सुविधा की तरह ही, डेटा लोड होने के दौरान प्लेसहोल्डर दिखाए जा सकते हैं. इसके लिए, यह देखना होगा कि 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 पिक्सल है और यह उन सभी को व्यूपोर्ट में फ़िट कर सकता है. आइटम लोड होने और उनकी ऊंचाई बढ़ने के बाद, लेज़ी लेआउट उन सभी आइटम को खारिज कर देगा जिन्हें पहली बार में बिना किसी वजह के कंपोज़ किया गया था. ऐसा इसलिए, क्योंकि वे व्यूपोर्ट में फ़िट नहीं हो सकते. इससे बचने के लिए, आपको अपने आइटम के लिए डिफ़ॉल्ट साइज़ सेट करना चाहिए, ताकि लेज़ी लेआउट यह सही तरीके से हिसाब लगा सके कि व्यूपोर्ट में कितने आइटम फ़िट हो सकते हैं:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

जब डेटा एसिंक्रोनस तरीके से लोड होने के बाद, आपको अपने आइटम के साइज़ के बारे में पता चल जाता है, तो यह पक्का करना एक अच्छा तरीका है कि लोड होने से पहले और बाद में, आपके आइटम का साइज़ एक जैसा रहे. उदाहरण के लिए, कुछ प्लेसहोल्डर जोड़कर ऐसा किया जा सकता है. इससे स्क्रोल करने की सही पोज़िशन बनाए रखने में मदद मिलेगी.

एक ही दिशा में स्क्रोल किए जा सकने वाले कॉम्पोनेंट को नेस्ट करने से बचें

यह सिर्फ़ उन मामलों पर लागू होता है जब स्क्रोल किए जा सकने वाले चाइल्ड एलिमेंट को, उसी दिशा में स्क्रोल किए जा सकने वाले पैरंट एलिमेंट में नेस्ट किया जाता है और चाइल्ड एलिमेंट का साइज़ पहले से तय नहीं होता है. उदाहरण के लिए, वर्टिकल तौर पर स्क्रोल किए जा सकने वाले Column पैरंट में, तय की गई ऊंचाई के बिना किसी बच्चे LazyColumn को नेस्ट करने की कोशिश करना:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

इसके बजाय, सभी कंपोज़ेबल को एक पैरंट LazyColumn में रैप करके और उसके डीएसएल का इस्तेमाल करके, एक जैसा नतीजा पाया जा सकता है. इससे अलग-अलग तरह का कॉन्टेंट पास किया जा सकता है. इससे एक ही जगह पर, एक आइटम के साथ-साथ सूची के कई आइटम भी दिखाए जा सकते हैं:

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

एक आइटम में कई एलिमेंट न डालें

इस उदाहरण में, दूसरा आइटम लैंबडा एक ब्लॉक में दो आइटम दिखाता है:

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 से शुरू करके, अपने LazyLayout की परफ़ॉर्मेंस को बेहतर बनाने के लिए, अपनी सूचियों या ग्रिड में contentType जोड़ें. इससे आपको लेआउट के हर आइटम के लिए कॉन्टेंट टाइप तय करने की सुविधा मिलती है. ऐसा तब किया जाता है, जब आपको अलग-अलग तरह के कई आइटम वाली सूची या ग्रिड बनानी हो:

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

contentType एट्रिब्यूट की वैल्यू देने पर, Compose सिर्फ़ एक जैसे आइटम के बीच कंपोज़िशन का फिर से इस्तेमाल कर पाता है. मिलते-जुलते स्ट्रक्चर वाले आइटम को फिर से इस्तेमाल करना ज़्यादा असरदार होता है. इसलिए, कॉन्टेंट टाइप की जानकारी देने से यह पक्का होता है कि Compose, टाइप A वाले आइटम के ऊपर टाइप B वाला आइटम कंपोज़ करने की कोशिश न करे. इससे कंपोज़िशन को फिर से इस्तेमाल करने और लेज़ी लेआउट की परफ़ॉर्मेंस के फ़ायदों को ज़्यादा से ज़्यादा बढ़ाने में मदद मिलती है.

परफ़ॉर्मेंस मेज़र करना

लेज़ी लेआउट की परफ़ॉर्मेंस का सही आकलन सिर्फ़ तब किया जा सकता है, जब वह रिलीज़ मोड में चल रहा हो और R8 ऑप्टिमाइज़ेशन चालू हो. डीबग बिल्ड पर, लेज़ी लेआउट की स्क्रोलिंग धीमी दिख सकती है. इस बारे में ज़्यादा जानने के लिए, Compose की परफ़ॉर्मेंस लेख पढ़ें.

अन्य संसाधन