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

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

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

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

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

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

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

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

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

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

अगर आपके डिज़ाइन में सिर्फ़ कुछ आइटम के लिए नॉन-स्टैंडर्ड डाइमेंशन की ज़रूरत है, तो आइटम के लिए कस्टम कॉलम स्पैन देने के लिए, ग्रिड सपोर्ट का इस्तेमाल किया जा सकता है. LazyGridScope DSL item और items तरीकों के span पैरामीटर की मदद से, कॉलम स्पैन तय करें. maxLineSpan, स्पैन स्कोप की वैल्यू में से एक है. यह खास तौर पर तब काम आता है, जब अडैप्टिव साइज़िंग का इस्तेमाल किया जा रहा हो. ऐसा इसलिए, क्योंकि कॉलम की संख्या तय नहीं होती. इस उदाहरण में, पूरी लाइन को स्पैन करने का तरीका बताया गया है:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

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

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

यहां दिया गया स्निपेट, हर आइटम के लिए 200.dp की चौड़ाई के साथ LazyVerticalStaggeredGrid का इस्तेमाल करने का एक बुनियादी उदाहरण है:

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() एक्सटेंशन फ़ंक्शन का इस्तेमाल कर सकते हैं. इसके बाद, रिटर्न किए गए LazyPagingItems को अपने LazyColumn में items() को पास कर सकते हैं. व्यू में पेजिंग की सुविधा की तरह ही, डेटा लोड होने के दौरान प्लेसहोल्डर दिखाए जा सकते हैं. इसके लिए, यह देखना होगा कि 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
        // ...
    )
}

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

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

यह सिर्फ़ उन मामलों में लागू होता है जब स्क्रोल किए जा सकने वाले चाइल्ड एलिमेंट को, पहले से तय साइज़ के बिना, उसी दिशा में स्क्रोल किए जा सकने वाले पैरंट एलिमेंट के अंदर नेस्ट किया जाता है. उदाहरण के लिए, वर्टिकल स्क्रोल किए जा सकने वाले 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)
    ) {
        // ...
    }
}

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

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

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

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

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

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

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

कस्टम अरेंजमेंट का इस्तेमाल करना

आम तौर पर, लेज़ी लिस्ट में कई आइटम होते हैं. साथ ही, वे स्क्रोलिंग कंटेनर के साइज़ से ज़्यादा जगह लेते हैं. हालांकि, जब आपकी सूची में कुछ ही आइटम हों, तो आपके डिज़ाइन में इस बात की ज़्यादा खास ज़रूरतें हो सकती हैं कि उन्हें व्यूपोर्ट में कैसे रखा जाए.

इसके लिए, कस्टम वर्टिकल Arrangement का इस्तेमाल किया जा सकता है. साथ ही, इसे LazyColumn को पास किया जा सकता है. यहां दिए गए उदाहरण में, TopWithFooter ऑब्जेक्ट को सिर्फ़ arrange तरीके को लागू करना होगा. सबसे पहले, यह आइटम को एक के बाद एक रखेगा. दूसरा, अगर इस्तेमाल की गई कुल ऊंचाई, व्यूपोर्ट की ऊंचाई से कम है, तो यह फ़ुटर को सबसे नीचे रखेगा:

object TopWithFooter : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

contentType जोड़ें

Compose 1.2 से, अपने लेज़ी लेआउट की परफ़ॉर्मेंस को बेहतर बनाने के लिए, अपनी सूचियों या ग्रिड में contentType जोड़ें. इससे आपको लेआउट के हर आइटम के लिए कॉन्टेंट टाइप तय करने की सुविधा मिलती है. ऐसा तब होता है, जब आपको अलग-अलग तरह के कई आइटम वाली सूची या ग्रिड बनाना हो:

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

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

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

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

अन्य संसाधन