Scenes ব্যবহার করে কাস্টম লেআউট তৈরি করা

Navigation 3, সিন-এর মাধ্যমে আপনার অ্যাপের UI ফ্লো ম্যানেজ করার জন্য একটি শক্তিশালী ও নমনীয় সিস্টেম নিয়ে আসে। দৃশ্য আপনাকে অত্যন্ত কাস্টমাইজ করা লেআউট তৈরি করতে, বিভিন্ন স্ক্রিন সাইজের সাথে মানিয়ে নিতে এবং জটিল মাল্টি-প্যানে অভিজ্ঞতা নির্বিঘ্নে ম্যানেজ করতে দেয়।

দৃশ্য বোঝা

নেভিগেশন ৩-এ, Scene হল মৌলিক একক যা এক বা একাধিক NavEntry ইনস্ট্যান্স রেন্ডার করে। Scene-কে আপনার UI-এর একটি স্বতন্ত্র ভিজ্যুয়াল স্টেট বা বিভাগ হিসেবে ভাবুন, যেখানে আপনার ব্যাক স্ট্যাক থেকে কন্টেন্ট দেখানো ও ম্যানেজ করা যায়।

প্রতিটি Scene ইনস্ট্যান্সকে তার key এবং Scene-এর ক্লাস দ্বারা অনন্যভাবে শনাক্ত করা হয়। এই অনন্য শনাক্তকারীটি খুবই গুরুত্বপূর্ণ কারণ এটি Scene পরিবর্তন হলে টপ-লেভেল অ্যানিমেশন চালায়।

Scene ইন্টারফেসের নিম্নলিখিত প্রপার্টি আছে:

  • key: Any: এই নির্দিষ্ট Scene ইনস্ট্যান্সের জন্য অনন্য শনাক্তকারী। এই কী, Scene-এর ক্লাসের সাথে মিলিত হয়ে, স্বতন্ত্রতা নিশ্চিত করে, মূলত অ্যানিমেশনের উদ্দেশ্যে।
  • entries: List<NavEntry<T>>: এটি NavEntry অবজেক্টের একটি তালিকা যা Scene দেখানোর জন্য দায়ী। গুরুত্বপূর্ণভাবে, ট্রানজিশনের সময় (যেমন, শেয়ার করা এলিমেন্ট ট্রানজিশন) একাধিক Scenes-এ একই NavEntry দেখানো হলে, সেটির কন্টেন্ট শুধুমাত্র সবচেয়ে সাম্প্রতিক টার্গেট Scene-এর মাধ্যমে রেন্ডার করা হবে যেটি এটি দেখাচ্ছে।
  • previousEntries: List<NavEntry<T>>: এই প্রপার্টি সেইসব NavEntryকে সংজ্ঞায়িত করে যেগুলি বর্তমান Scene থেকে "আগের পৃষ্ঠায় যান" অ্যাকশন নিলে পাওয়া যাবে। সঠিক প্রেডিক্টিভ ব্যাক স্টেট গণনা করার জন্য এটি অপরিহার্য, NavDisplay-কে সঠিক পূর্ববর্তী স্টেট অনুমান করতে এবং ট্রানজিশন করতে দেয়, যা আলাদা ক্লাস, কী বা দু'টিই সহ কোনও সিন হতে পারে।
  • content: @Composable () -> Unit: এটি হল কম্পোজ করা যায় এমন ফাংশন যেখানে আপনি কীভাবে Scene তার entries এবং Scene-এর সাথে সম্পর্কিত যেকোনও আশেপাশের UI এলিমেন্ট রেন্ডার করে তা নির্ধারণ করেন।
  • metadata: Map<String, Any>: NavDisplay-এর মতো অন্যান্য লাইব্রেরি কম্পোনেন্টকে দৃশ্য-নির্দিষ্ট তথ্য প্রদান করে। ডিফল্ট হিসেবে, entries-এর মধ্যে শেষ NavEntry-এর metadata রিটার্ন করে।

কাস্টম Scene-এ equals ও hashCode প্রয়োগ করা

কাস্টম Scene ইমপ্লিমেন্টেশনকে নিম্নলিখিত বিষয়গুলি সাপোর্ট করার জন্য equals ও hashCode সঠিকভাবে ইমপ্লিমেন্ট করতে হবে:

  • সিন স্টেট ট্রানজিশন: প্রতিবার সিন স্ট্র্যাটেজির তালিকা দ্বারা কোনও সিন গণনা করা হলে, NavDisplay নতুন সিন ও আগের সিনের মধ্যে মিল আছে কিনা তা চেক করে। এটি কোনও পরিবর্তন শনাক্ত করলে, SeekableTransition দৃশ্যগুলির মধ্যে ট্রানজিশনের জন্য ব্যবহৃত হয় এমন আইটেম আপডেট করে, যা দৃশ্যগুলির লাইফসাইকেল স্টেটকে প্রভাবিত করে।
  • ওভারলে ম্যানেজমেন্ট: OverlaySceneর (যেমন ডায়ালগ) জন্য, NavDisplay ওভারলে লাইফসাইকেল ও এক্সিট অ্যানিমেশন ট্র্যাক করতে, সিন অবজেক্টকেই key হিসেবে ব্যবহার করে।

key, entries ও previousEntries-এর মতো দৃশ্যের কন্টেন্টকে সংজ্ঞায়িত করে এমন সব প্রপার্টি equals ও hashCode ইমপ্লিমেন্টেশনে অন্তর্ভুক্ত আছে কিনা তা যাচাই করুন। সাধারণত, এইসব পদ্ধতির প্রয়োগে কলব্যাক (যেমন onBack) অন্তর্ভুক্ত করা এড়িয়ে চলুন, কারণ এগুলি লজিক্যাল সিন স্টেট পরিবর্তন না করেই ইনস্ট্যান্স পরিবর্তন করতে পারে।

সিন স্ট্র্যাটেজি সম্পর্কে বোঝা

SceneStrategy হল এমন একটি মেকানিজম যা নির্ধারণ করে যে ব্যাক স্ট্যাক থেকে নেওয়া NavEntry-এর দেওয়া তালিকা কীভাবে সাজানো হবে এবং Scene-এ ট্রানজিট করা হবে। মূলত, বর্তমান ব্যাক স্ট্যাক এন্ট্রি দেখানো হলে, একটি SceneStrategy নিজেকে দুটি মূল প্রশ্ন করে:

  1. আমি কি এইসব এন্ট্রি থেকে Scene তৈরি করতে পারব? SceneStrategy যদি নির্ধারণ করে যে এটি প্রদত্ত NavEntryগুলি ম্যানেজ করতে পারবে এবং একটি অর্থপূর্ণ Scene (যেমন, একটি ডায়ালগ বা মাল্টি-প্যানে লেআউট) তৈরি করতে পারবে, তাহলে এটি প্রসেস করা শুরু করে। অন্যথায়, এটি null রিটার্ন করে, যার ফলে অন্যান্য স্ট্র্যাটেজি Scene তৈরি করার সুযোগ পায়।
  2. যদি তাই হয়, তাহলে Scene?-এ আমি কীভাবে সেইসব এন্ট্রি সাজাব?SceneStrategy কোনও এন্ট্রি ম্যানেজ করার ব্যাপারে সম্মতি জানালে, সেটি Scene তৈরি করার দায়িত্ব নেয় এবং নির্দিষ্ট করা NavEntry-গুলি কীভাবে সেই Scene-এর মধ্যে দেখানো হবে তা নির্ধারণ করে।

SceneStrategy-এর মূল বিষয় হল এর calculateScene পদ্ধতি:

@Composable
public fun calculateScene(
    entries: List<NavEntry<T>>,
    onBack: (count: Int) -> Unit,
): Scene<T>?

এই পদ্ধতি হল SceneStrategyScope-এর একটি এক্সটেনশন ফাংশন যা ব্যাক স্ট্যাক থেকে বর্তমান List<NavEntry<T>> নেয়। প্রদত্ত এন্ট্রি থেকে এটি সফলভাবে একটি তৈরি করতে পারলে, Scene<T> ফেরত দেওয়া উচিত অথবা এটি তৈরি করতে না পারলে null ফেরত দেওয়া উচিত।

SceneStrategy-এর প্রয়োজন হতে পারে এমন SceneStrategyScope-এর ঐচ্ছিক আর্গুমেন্ট মেনে চলার দায়িত্ব SceneStrategyScope-এর, যেমন onBack কলব্যাক।

দৃশ্য ও দৃশ্য কৌশল কীভাবে একসাথে কাজ করে

NavDisplay হল কেন্দ্রীয় কম্পোজ করার মতো আইটেম যা আপনার ব্যাক স্ট্যাক লক্ষ্য করে এবং উপযুক্ত Scene নির্ধারণ ও রেন্ডার করতে এক বা একাধিক SceneStrategy ব্যবহার করে।

NavDisplay's sceneStrategies প্যারামিটার SceneStrategy ইনস্ট্যান্সের একটি তালিকা প্রত্যাশা করে যা দেখানোর জন্য Scene গণনা করার জন্য দায়ী। প্রদত্ত স্ট্র্যাটেজি অনুযায়ী কোনও Scene গণনা করা না গেলে, NavDisplay অটোমেটিক ডিফল্ট হিসেবে SinglePaneSceneStrategy ব্যবহার করে।

ইন্টার‍্যাকশনের ব্রেকডাউন এখানে দেওয়া হল:

  • আপনার ব্যাক স্ট্যাক থেকে কী যোগ করলে বা সরিয়ে দিলে (যেমন, backStack.add() বা backStack.removeLastOrNull() ব্যবহার করে), NavDisplay এইসব পরিবর্তন লক্ষ্য করে।
  • NavDisplay কনফিগার করা sceneStrategies-কে NavEntry-এর বর্তমান তালিকা (ব্যাক স্ট্যাক কী থেকে প্রাপ্ত) ক্রমানুসারে পাস করে, Scene রিটার্ন না করা পর্যন্ত প্রতিটিতে calculateScene কল করে।
  • SceneStrategy সফলভাবে Scene রিটার্ন করলে, NavDisplay তারপরে সেই Scene-এর content রেন্ডার করে। NavDisplay, Scene-এর প্রপার্টির উপর ভিত্তি করে অ্যানিমেশন ও প্রেডিক্টিভ ব্যাকও ম্যানেজ করে।

উদাহরণ: সিঙ্গেল প্যানেল লেআউট (ডিফল্ট আচরণ)

সবচেয়ে সহজ কাস্টম লেআউট হল সিঙ্গেল-পেন ডিসপ্লে, যা অন্য কোনও SceneStrategy অগ্রাধিকার না পেলে ডিফল্ট আচরণ হিসেবে কাজ করে।

data class SinglePaneScene<T : Any>(
    override val key: Any,
    val entry: NavEntry<T>,
    override val previousEntries: List<NavEntry<T>>,
) : Scene<T> {
    override val entries: List<NavEntry<T>> = listOf(entry)
    override val content: @Composable () -> Unit = { entry.Content() }
}

/**
 * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the
 * list.
 */
public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> {
    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? =
        SinglePaneScene(
            key = entries.last().contentKey,
            entry = entries.last(),
            previousEntries = entries.dropLast(1)
        )
}

উদাহরণ: সাধারণ তালিকা-বিবরণ লেআউট (কাস্টম সিন ও স্ট্র্যাটেজি)

এই উদাহরণে দেখানো হয়েছে যে কীভাবে এমন একটি তালিকা-বিবরণ লেআউট তৈরি করতে হয় যা দুটি শর্তের উপর ভিত্তি করে অ্যাক্টিভেট করা হয়:

  1. দুটি প্যানেল (অর্থাৎ, অন্তত WIDTH_DP_MEDIUM_LOWER_BOUND) কাজ করার মতো উইন্ডো উইথ যথেষ্ট চওড়া।
  2. ব্যাক স্ট্যাকে এমন এন্ট্রি থাকে যেগুলি নির্দিষ্ট মেটাডেটা ব্যবহার করে তালিকা-বিবরণ লেআউটে দেখানোর জন্য নিজেদের উপযুক্ত হিসেবে ঘোষণা করেছে।

নিচে দেওয়া স্নিপেটটি হল ListDetailScene.kt-এর সোর্স কোড এবং এতে ListDetailScene ও ListDetailSceneStrategy দুটিই আছে:

// --- ListDetailScene ---
/**
 * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split.
 *
 */
data class ListDetailScene<T : Any>(
    override val key: Any,
    override val previousEntries: List<NavEntry<T>>,
    val listEntry: NavEntry<T>,
    val detailEntry: NavEntry<T>,
) : Scene<T> {
    override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry)
    override val content: @Composable (() -> Unit) = {
        Row(modifier = Modifier.fillMaxSize()) {
            Column(modifier = Modifier.weight(0.4f)) {
                listEntry.Content()
            }
            Column(modifier = Modifier.weight(0.6f)) {
                detailEntry.Content()
            }
        }
    }
}

@Composable
fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass

    return remember(windowSizeClass) {
        ListDetailSceneStrategy(windowSizeClass)
    }
}

// --- ListDetailSceneStrategy ---
/**
 * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item
 * is the backstack is a detail, and before it, at any point in the backstack is a list.
 */
class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> {

    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? {

        if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
            return null
        }

        val detailEntry =
            entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null
        val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null

        // We use the list's contentKey to uniquely identify the scene.
        // This allows the detail panes to be displayed instantly through recomposition, rather than
        // having NavDisplay animate the whole scene out when the selected detail item changes.
        val sceneKey = listEntry.contentKey

        return ListDetailScene(
            key = sceneKey,
            previousEntries = entries.dropLast(1),
            listEntry = listEntry,
            detailEntry = detailEntry
        )
    }

    object ListKey : NavMetadataKey<Boolean>
    object DetailKey : NavMetadataKey<Boolean>
    companion object {

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun listPane() = metadata {
            put(ListKey, true)
        }

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun detailPane() = metadata {
            put(DetailKey, true)
        }
    }
}

আপনার NavDisplay-এ এই ListDetailSceneStrategy ব্যবহার করতে, আপনি যে এন্ট্রি তালিকা লেআউট হিসেবে দেখাতে চান তার জন্য ListDetailScene.listPane() মেটাডেটা এবং আপনি যে এন্ট্রি বিবরণ লেআউট হিসেবে দেখাতে চান তার জন্য ListDetailScene.detailPane() অন্তর্ভুক্ত করতে আপনার entryProvider কল পরিবর্তন করুন। তারপর, ListDetailSceneStrategy()-কে আপনার sceneStrategy হিসেবে দিন, সিঙ্গেল-পেন পরিস্থিতির জন্য ডিফল্ট ফলব্যাকের উপর নির্ভর করে:

// Define your navigation keys
@Serializable
data object ConversationList : NavKey

@Serializable
data class ConversationDetail(val id: String) : NavKey

@Composable
fun MyAppContent() {
    val backStack = rememberNavBackStack(ConversationList)
    val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        sceneStrategies = listOf(listDetailStrategy),
        entryProvider = entryProvider {
            entry<ConversationList>(
                metadata = ListDetailSceneStrategy.listPane()
            ) {
                Column(modifier = Modifier.fillMaxSize()) {
                    Text(text = "I'm a Conversation List")
                    Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) {
                        Text(text = "Open detail")
                    }
                }
            }
            entry<ConversationDetail>(
                metadata = ListDetailSceneStrategy.detailPane()
            ) {
                Text(text = "I'm a Conversation Detail")
            }
        }
    )
}

private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) {

    // Remove any existing detail routes, then add the new detail route
    removeIf { it is ConversationDetail }
    add(detailRoute)
}

আপনি নিজের তালিকা-বিবরণ দৃশ্য তৈরি করতে না চাইলে, আপনি মেটেরিয়াল তালিকা-বিবরণ দৃশ্য ব্যবহার করতে পারেন, যা যুক্তিসঙ্গত বিবরণ এবং প্লেসহোল্ডারের জন্য সহায়তা সহ আসে, যেমনটি পরবর্তী বিভাগে দেখানো হয়েছে।

মেটেরিয়াল অ্যাডাপ্টিভ সিনের মধ্যে তালিকা-বিবরণ কন্টেন্ট দেখানো

তালিকা-বিবরণ ব্যবহারের ক্ষেত্রে, androidx.compose.material3.adaptive:adaptive-navigation3 আর্টিফ্যাক্ট একটি ListDetailSceneStrategy প্রদান করে যা তালিকা-বিবরণ Scene তৈরি করে। এটি Scene জটিল মাল্টি-পেন অ্যারেঞ্জমেন্ট (তালিকা, বিবরণ ও অতিরিক্ত পেন) অটোমেটিক ম্যানেজ করে এবং উইন্ডোর সাইজ ও ডিভাইসের অবস্থার উপর ভিত্তি করে সেগুলিকে অ্যাডজাস্ট করে।

Material তালিকা-বিবরণ Scene তৈরি করতে, এইসব ধাপ অনুসরণ করুন:

  1. ডিপেন্ডেন্সি যোগ করুন: আপনার প্রোজেক্টের build.gradle.kts ফাইলে androidx.compose.material3.adaptive:adaptive-navigation3 যোগ করুন।
  2. ListDetailSceneStrategy মেটাডেটা দিয়ে আপনার এন্ট্রিকে সংজ্ঞায়িত করুন: উপযুক্ত প্যানে ডিসপ্লে করার জন্য আপনার NavEntrys চিহ্নিত করতে listPane(), detailPane() এবং extraPane() ব্যবহার করুন। এছাড়াও, কোনও আইটেম বেছে নেওয়া না হলে listPane() হেল্পার আপনাকে detailPlaceholder নির্দিষ্ট করতে দেয়।
  3. rememberListDetailSceneStrategy() ব্যবহার করুন: এই কম্পোজেবল ফাংশন আগে থেকে কনফিগার করা ListDetailSceneStrategy প্রদান করে যা NavDisplay ব্যবহার করতে পারে।

নিচের স্নিপেটটি Activity-এর ব্যবহার দেখানোর ListDetailSceneStrategy একটি নমুনা:

@Serializable
object ProductList : NavKey

@Serializable
data class ProductDetail(val id: String) : NavKey

@Serializable
data object Profile : NavKey

class MaterialListDetailActivity : ComponentActivity() {

    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Scaffold { paddingValues ->
                val backStack = rememberNavBackStack(ProductList)
                val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()

                NavDisplay(
                    backStack = backStack,
                    modifier = Modifier.padding(paddingValues),
                    onBack = { backStack.removeLastOrNull() },
                    sceneStrategies = listOf(listDetailStrategy),
                    entryProvider = entryProvider {
                        entry<ProductList>(
                            metadata = ListDetailSceneStrategy.listPane(
                                detailPlaceholder = {
                                    ContentYellow("Choose a product from the list")
                                }
                            )
                        ) {
                            ContentRed("Welcome to Nav3") {
                                Button(onClick = {
                                    backStack.add(ProductDetail("ABC"))
                                }) {
                                    Text("View product")
                                }
                            }
                        }
                        entry<ProductDetail>(
                            metadata = ListDetailSceneStrategy.detailPane()
                        ) { product ->
                            ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) {
                                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                                    Button(onClick = {
                                        backStack.add(Profile)
                                    }) {
                                        Text("View profile")
                                    }
                                }
                            }
                        }
                        entry<Profile>(
                            metadata = ListDetailSceneStrategy.extraPane()
                        ) {
                            ContentGreen("Profile")
                        }
                    }
                )
            }
        }
    }
}

ছবি ১. Material list-detail Scene-এ রান করা কন্টেন্টের উদাহরণ।