লেজি লিস্ট ও লেজি গ্রিড

অনেক অ্যাপকে আইটেমের কালেকশন দেখাতে হয়। এই ডকুমেন্ট ব্যাখ্যা করে যে কীভাবে আপনি 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-এর বেশিরভাগ লেআউটের থেকে Lazy কম্পোনেন্ট আলাদা। @Composable কন্টেন্ট ব্লক প্যারামিটার গ্রহণ করার পরিবর্তে, অ্যাপকে সরাসরি কম্পোজেবল এমিট করার অনুমতি দিয়ে, লেজি কম্পোনেন্ট LazyListScope.() ব্লক প্রদান করে। এই LazyListScope ব্লক একটি DSL অফার করে যা অ্যাপগুলিকে আইটেমের কন্টেন্ট বর্ণনা করার অনুমতি দেয়। লেআউট ও স্ক্রল পজিশন অনুযায়ী প্রতিটি আইটেমের কন্টেন্ট যোগ করার দায়িত্ব Lazy কম্পোনেন্টের উপর বর্তায়।

LazyListScope DSL

LazyListScope-এর DSL লেআউটে আইটেম বর্ণনা করার জন্য অনেকগুলি ফাংশন প্রদান করে। সবচেয়ে সহজভাবে, 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 কম্পোজেবল গ্রিডে আইটেম দেখানোর জন্য সহায়তা প্রদান করে। একটি লেজি ভার্টিকাল গ্রিড এটির আইটেমগুলি একটি উল্লম্বভাবে স্ক্রোলযোগ্য কন্টেনারে প্রদর্শন করবে, যা একাধিক কলাম জুড়ে বিস্তৃত, যখন লেজি হরাইজন্টাল গ্রিডগুলির অনুভূমিক অক্ষে একই আচরণ থাকবে।

গ্রিডে তালিকার মতোই শক্তিশালী API ক্ষমতা রয়েছে এবং তারা কন্টেন্ট বর্ণনা করার জন্য খুব একই রকম DSL - 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 হল কম্পোজেবল যা আপনাকে আইটেমের ধীরে ধীরে লোড করা, স্ট্যাগার্ড গ্রিড তৈরি করতে দেয়। একটি লেজি ভার্টিক্যাল স্ট্যাগার্ড গ্রিড তার আইটেমগুলি একটি উল্লম্বভাবে স্ক্রোলযোগ্য কন্টেনারে প্রদর্শন করে যা একাধিক কলাম জুড়ে বিস্তৃত এবং পৃথক আইটেমগুলিকে বিভিন্ন উচ্চতার হতে দেয়। লেজি হরাইজন্টাল গ্রিডের হরাইজন্টাল অ্যাক্সিসে একই আচরণ থাকে এবং আইটেমের প্রস্থ আলাদা হয়।

নিম্নলিখিত স্নিপেটটি হল আইটেম প্রতি 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-এ প্রিমিটিভ, enum বা Parcelable-এর মতো টাইপ কাজ করে।

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Bundle-এর মাধ্যমে 'কী' অবশ্যই কাজ করতে হবে যাতে অ্যাক্টিভিটি আবার তৈরি করার সময়, এমনকি আপনি এই আইটেম থেকে দূরে স্ক্রল করে আবার ফিরে এলেও, আইটেম কম্পোজেবলের মধ্যে rememberSaveable রিস্টোর করা যায়।

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

আইটেম অ্যানিমেশন

আপনি RecyclerView উইজেট ব্যবহার করে থাকলে, আপনি জানবেন যে এটি অটোমেটিক আইটেম পরিবর্তন অ্যানিমেট করে। আইটেম আবার সাজানোর জন্য লেজি লেআউট একই কার্যকারিতা প্রদান করে। API-টি সহজ - আপনাকে শুধু আইটেম কন্টেন্টে 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 তালিকা আবার সাজানোর বোতাম দেখায়। এটি ক্রম রিসেট করার জন্য এবং দৈর্ঘ্য বা বর্ণানুক্রমিকভাবে তালিকা সাজানোর জন্য lambda ফাংশন গ্রহণ করে।
  • ListAnimatedItems, ListAnimatedItems কম্পোজ করার উপযুক্ত আইটেমকে কল করে, data তালিকা পাস করে যাতে স্ট্রিংয়ের অ্যানিমেটেড তালিকা দেখানো যায়। data অন্য কোথাও নির্দিষ্ট করা হয়েছে।

এই স্নিপেট আইটেম যোগ করুন এবং আইটেম মুছুন বোতাম সহ একটি UI তৈরি করে:

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

অন্য UI কম্পোজেবল আপডেট করার প্রয়োজন হলে, কম্পোজিশনে সরাসরি স্টেট রিড করা উপযোগী হয়, তবে এমন পরিস্থিতিও থাকে যেখানে একই কম্পোজিশনে ইভেন্ট ম্যানেজ করার প্রয়োজন হয় না। এর একটি সাধারণ উদাহরণ হল, ব্যবহারকারী কোনও নির্দিষ্ট পয়েন্টের পরে স্ক্রল করলে অ্যানালিটিক্স ইভেন্ট পাঠানো। এটি দক্ষতার সাথে ম্যানেজ করতে, আমরা একটি 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)
            }
        }
    )
}

বড় ডেটা-সেট (পেজিং)

পেজিং লাইব্রেরি অ্যাপকে আইটেমের বড় তালিকা সাপোর্ট করতে সাহায্য করে, প্রয়োজন অনুযায়ী তালিকার ছোট ছোট অংশ লোড করে ও দেখায়। পেজিং ৩.০ এবং পরবর্তী ভার্সন androidx.paging:paging-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()
            }
        }
    }
}

লেজি লেআউট ব্যবহার করার ব্যাপারে পরামর্শ

আপনার লেজি লেআউট যাতে প্রত্যাশা অনুযায়ী কাজ করে তা নিশ্চিত করতে আপনি কিছু পরামর্শ বিবেচনা করতে পারেন।

০-পিক্সেল সাইজের আইটেম ব্যবহার করা এড়িয়ে চলুন

এটি এমন পরিস্থিতিতে ঘটতে পারে যেখানে, উদাহরণস্বরূপ, আপনি অ্যাসিঙ্ক্রোনাসভাবে কিছু ডেটা যেমন ছবি, পরে আপনার তালিকার আইটেমগুলি পূরণ করার জন্য আশা করেন। এর ফলে Lazy লেআউট প্রথম পরিমাপে তার সব আইটেম কম্পোজ করবে, কারণ সেগুলির উচ্চতা ০ পিক্সেল এবং এটি ভিউপোর্টে সব আইটেম ফিট করতে পারবে। আইটেমগুলি লোড হয়ে গেলে এবং তাদের উচ্চতা প্রসারিত হলে, Lazy লেআউটগুলি অন্যান্য সমস্ত আইটেম বাতিল করে দেবে যা প্রথমবার অপ্রয়োজনীয়ভাবে কম্পোজ করা হয়েছে কারণ সেগুলি আসলে ভিউপোর্টে ফিট করতে পারে না। এটি এড়াতে, আপনার আইটেমের ডিফল্ট সাইজ সেট করা উচিত, যাতে Lazy লেআউট ভিউ পোর্টে আসলে কতগুলি আইটেম ফিট করতে পারে তার সঠিক গণনা করতে পারে:

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

একটি আইটেমে একাধিক এলিমেন্ট যোগ করার সময় সতর্ক থাকুন

এই উদাহরণে, দ্বিতীয় আইটেম ল্যাম্বডা একটি ব্লকে ২টি আইটেম এমিট করে:

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

কাস্টম ব্যবস্থা ব্যবহার করার কথা বিবেচনা করুন

সাধারণত Lazy লিস্টে অনেক আইটেম থাকে এবং সেগুলি স্ক্রোলিং কন্টেনারের সাইজের চেয়ে বেশি জায়গা দখল করে। তবে, আপনার তালিকায় অল্প আইটেম থাকলে, ভিউপোর্টে সেগুলি কীভাবে পজিশন করা হবে সেই ব্যাপারে আপনার ডিজাইনের আরও নির্দিষ্ট প্রয়োজনীয়তা থাকতে পারে।

এটি করতে, আপনি কাস্টম ভার্টিক্যাল ব্যবহার করতে পারেন 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 লেআউটের পারফর্ম্যান্স সর্বাধিক করতে, আপনার তালিকা বা গ্রিডে contentType যোগ করার কথা বিবেচনা করুন। এটি আপনাকে লেআউটের প্রতিটি আইটেমের জন্য কন্টেন্টের ধরন নির্দিষ্ট করতে দেয়, এমন ক্ষেত্রে যেখানে আপনি একাধিক বিভিন্ন ধরনের আইটেম নিয়ে গঠিত একটি তালিকা বা গ্রিড কম্পোজ করছেন:

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

আপনি contentType প্রদান করলে, Compose শুধুমাত্র একই ধরনের আইটেমের মধ্যে কম্পোজিশন আবার ব্যবহার করতে পারে। একই ধরনের স্ট্রাকচারের আইটেম কম্পোজ করার সময় পুনর্ব্যবহার করা আরও বেশি কার্যকর হয়, কন্টেন্টের ধরন প্রদান করলে এটি নিশ্চিত হয় যে Compose, B ধরনের সম্পূর্ণ আলাদা আইটেমের উপরে A ধরনের আইটেম কম্পোজ করার চেষ্টা করে না। এটি কম্পোজিশন আবার ব্যবহার করার সুবিধা এবং আপনার Lazy লেআউটের পারফর্ম্যান্স সর্বাধিক করতে সাহায্য করে।

পারফর্ম্যান্স পরিমাপ করা

আপনি শুধুমাত্র তখনই Lazy লেআউটের পারফর্ম্যান্স নির্ভরযোগ্যভাবে পরিমাপ করতে পারবেন যখন এটি রিলিজ মোডে এবং R8 অপ্টিমাইজেশন চালু রেখে চালানো হয়। ডিবাগ বিল্ডে, লেজি লেআউট স্ক্রলিং ধীরগতির মনে হতে পারে। এই বিষয়ে আরও জানতে, Compose পারফর্ম্যান্স পড়ুন।

অতিরিক্ত রিসোর্স