তালিকা এবং গ্রিড

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

কম্পোজের বেশিরভাগ লেআউট থেকে লেজি কম্পোনেন্টগুলো আলাদা। অ্যাপগুলোকে সরাসরি কম্পোজেবল তৈরি করার সুযোগ দিয়ে @Composable কন্টেন্ট ব্লক প্যারামিটার গ্রহণ করার পরিবর্তে, লেজি কম্পোনেন্টগুলো একটি LazyListScope.() ব্লক প্রদান করে। এই LazyListScope ব্লকটি একটি DSL (ডিজিটাল স্ট্রাকচার) সরবরাহ করে, যা অ্যাপগুলোকে আইটেমের বিষয়বস্তু বর্ণনা করার সুযোগ দেয়। এরপর লেআউট এবং স্ক্রল পজিশন অনুযায়ী প্রতিটি আইটেমের বিষয়বস্তু যোগ করার দায়িত্ব লেজি কম্পোনেন্টের উপর বর্তায়।

LazyListScope ডিএসএল

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

গ্রিডগুলোতে লিস্টের মতোই শক্তিশালী API সুবিধা রয়েছে এবং এর বিষয়বস্তু বর্ণনা করার জন্য এগুলো LazyGridScope.() মতো প্রায় একই ধরনের DSL ব্যবহার করে।

ফোনের একটি স্ক্রিনশট যেখানে ছবিগুলোর একটি গ্রিড দেখা যাচ্ছে।

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 প্যারামিটার দিয়ে কলাম স্প্যান নির্দিষ্ট করুন। 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()
)
Lazy staggered grid of images in 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 প্যারামিটারে একটি ব্লক প্রদান করে প্রতিটি আইটেমের জন্য একটি স্থিতিশীল ও অনন্য 'key' দিতে পারেন। একটি স্থিতিশীল 'key' প্রদান করলে ডেটা-সেট পরিবর্তনের পরেও আইটেমের অবস্থা সামঞ্জস্যপূর্ণ থাকে:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

কী (key) প্রদান করার মাধ্যমে, আপনি কম্পোজকে পুনর্বিন্যাস সঠিকভাবে পরিচালনা করতে সাহায্য করেন। উদাহরণস্বরূপ, যদি আপনার আইটেমটিতে মনে রাখা স্টেট (state) থাকে, তাহলে কী সেট করলে কম্পোজ আইটেমটির অবস্থান পরিবর্তনের সাথে সাথে এই স্টেটটিকেও সরিয়ে নিতে পারবে।

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

তবে, আইটেম কী (key) হিসেবে আপনি কী ধরনের ডেটা টাইপ ব্যবহার করতে পারবেন, তার একটি সীমাবদ্ধতা রয়েছে। কী-টির টাইপ অবশ্যই Bundle দ্বারা সমর্থিত হতে হবে; বান্ডেল হলো অ্যান্ড্রয়েডের এমন একটি ব্যবস্থা যা অ্যাক্টিভিটি পুনরায় তৈরি হওয়ার সময় তার স্টেটগুলো সংরক্ষণ করে। Bundle প্রিমিটিভ, এনাম বা পার্সেলেবলের মতো টাইপ সমর্থন করে।

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

আপনার আইটেমগুলোর জন্য কী (key) প্রদান করুন, যাতে সরানো উপাদানটির নতুন অবস্থান খুঁজে পাওয়া সম্ভব হয়।

উদাহরণ: লেজি লিস্টের আইটেমগুলোকে অ্যানিমেট করুন

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 ফাংশনটি তালিকার প্রতিটি আইটেমকে একটি অনন্য কী প্রদান করে। কম্পোজ এই কীগুলো ব্যবহার করে আইটেমগুলোকে ট্র্যাক করে এবং তাদের অবস্থানের পরিবর্তন শনাক্ত করে।
  • 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 "Add" এবং "Remove" বাটন প্রদর্শন করে। এটি বাটনগুলোকে সক্রিয়/নিষ্ক্রিয় করে এবং বাটন ক্লিকের বিষয়টিও পরিচালনা করে।
  • OrderButtons তালিকাটির ক্রম পরিবর্তন করার জন্য বাটনগুলো প্রদর্শন করে। এটি ক্রম রিসেট করার এবং তালিকাটিকে দৈর্ঘ্য অনুসারে বা বর্ণানুক্রমিকভাবে সাজানোর জন্য ল্যাম্বডা ফাংশনগুলো গ্রহণ করে।
  • ListAnimatedItems স্ট্রিং-এর অ্যানিমেটেড তালিকা প্রদর্শনের জন্য data তালিকাটি পাস করে ListAnimatedItems কম্পোজেবলকে কল করে। 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 মান false হয়, তাহলে সংশ্লিষ্ট বাটনটি নিষ্ক্রিয় থাকে।
  • 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 লাইব্রেরির মাধ্যমে কম্পোজ (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()
            }
        }
    }
}

লেজি লেআউট ব্যবহারের টিপস

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

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

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

@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 মধ্যে রেখে এবং এর 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) }
    // ...
}

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

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

এটি অর্জন করতে, আপনি কাস্টম ভার্টিকাল 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) শুধুমাত্র একই ধরনের আইটেমগুলোর মধ্যে কম্পোজিশন পুনঃব্যবহার করতে পারে। যেহেতু একই ধরনের কাঠামোর আইটেম কম্পোজ করলে পুনঃব্যবহার আরও বেশি কার্যকর হয়, তাই কন্টেন্ট টাইপগুলো প্রদান করা নিশ্চিত করে যে কম্পোজ যেন সম্পূর্ণ ভিন্ন ধরনের B টাইপের একটি আইটেমের উপরে A টাইপের একটি আইটেম কম্পোজ করার চেষ্টা না করে। এটি কম্পোজিশন পুনঃব্যবহারের সুবিধা এবং আপনার লেজি লেআউট (Lazy layout) পারফরম্যান্সকে সর্বোচ্চ করতে সাহায্য করে।

কর্মক্ষমতা পরিমাপ

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

অতিরিক্ত সম্পদ

{% হুবহু %} {% endverbatim %} {% হুবহু %} {% endverbatim %}