দৃশ্য ব্যবহার করে কাস্টম লেআউট তৈরি করুন

ন্যাভিগেশন ৩ আপনার অ্যাপের UI ফ্লো পরিচালনার জন্য সিন (Scenes) নামক একটি শক্তিশালী ও নমনীয় সিস্টেম নিয়ে এসেছে। সিন আপনাকে অত্যন্ত কাস্টমাইজড লেআউট তৈরি করতে, বিভিন্ন স্ক্রিন সাইজের সাথে মানিয়ে নিতে এবং জটিল মাল্টি-পেন অভিজ্ঞতা নির্বিঘ্নে পরিচালনা করতে সাহায্য করে।

দৃশ্যগুলি বুঝুন

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

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

Scene ইন্টারফেসের নিম্নলিখিত বৈশিষ্ট্যগুলো রয়েছে:

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

দৃশ্য কৌশল বুঝুন

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

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

একটি SceneStrategy এর মূল অংশ হলো এর calculateScene মেথড:

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

এই মেথডটি SceneStrategyScope এর একটি এক্সটেনশন ফাংশন, যা ব্যাক স্ট্যাক থেকে বর্তমান List<NavEntry<T>> গ্রহণ করে। প্রদত্ত এন্ট্রিগুলো থেকে সফলভাবে একটি Scene<T> তৈরি করতে পারলে এটি একটি Scene<T> রিটার্ন করবে, আর না পারলে null রিটার্ন করবে।

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

দৃশ্য এবং দৃশ্য কৌশলগুলি কীভাবে একসাথে কাজ করে

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

NavDisplay's sceneStrategy প্যারামিটারটি এমন একটি SceneStrategy প্রত্যাশা করে, যা প্রদর্শিতব্য Scene গণনা করার দায়িত্বে থাকে। যদি প্রদত্ত স্ট্র্যাটেজি (বা স্ট্র্যাটেজির শৃঙ্খল) দ্বারা কোনো Scene গণনা করা না হয়, তাহলে NavDisplay ডিফল্টরূপে স্বয়ংক্রিয়ভাবে একটি SinglePaneSceneStrategy ব্যবহার করতে ফিরে যায়।

মিথস্ক্রিয়াটির বিশদ বিবরণ নিচে দেওয়া হলো:

  • যখন আপনি আপনার ব্যাক স্ট্যাক থেকে কী (key) যোগ বা অপসারণ করেন (যেমন, backStack.add() বা backStack.removeLastOrNull() ব্যবহার করে), তখন NavDisplay এই পরিবর্তনগুলো লক্ষ্য করে।
  • NavDisplay ব্যাক স্ট্যাক কীগুলো থেকে প্রাপ্ত NavEntrys গুলোর বর্তমান তালিকাটি কনফিগার করা SceneStrategy's 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 এর সোর্স কোড এবং এতে ListDetailSceneListDetailSceneStrategy উভয়ই রয়েছে:

// --- ListDetailScene ---
/**
 * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split.
 *
 */
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 ব্যবহার করতে, আপনার entryProvider কলগুলিকে পরিবর্তন করুন। এর জন্য, যে এন্ট্রিটি আপনি লিস্ট লেআউট হিসাবে দেখাতে চান তার জন্য ListDetailScene.listPane() মেটাডেটা এবং যে এন্ট্রিটি আপনি ডিটেইল লেআউট হিসাবে দেখাতে চান তার জন্য ListDetailScene.detailPane() অন্তর্ভুক্ত করুন। তারপর, আপনার sceneStrategy হিসাবে ListDetailSceneStrategy() প্রদান করুন এবং সিঙ্গেল-পেন সিনারিওগুলির জন্য ডিফল্ট ফলব্যাকের উপর নির্ভর করুন।

// 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) তৈরি করে। এই Scene স্বয়ংক্রিয়ভাবে জটিল মাল্টি-পেন বিন্যাস (লিস্ট, ডিটেইল এবং অতিরিক্ত পেন) পরিচালনা করে এবং উইন্ডোর আকার ও ডিভাইসের অবস্থার উপর ভিত্তি করে সেগুলোকে অভিযোজিত করে।

একটি ম্যাটেরিয়াল লিস্ট-ডিটেইল Scene তৈরি করতে, এই ধাপগুলো অনুসরণ করুন:

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

নিম্নলিখিত কোড স্নিপেটটি ListDetailSceneStrategy এর ব্যবহার প্রদর্শনকারী একটি নমুনা Activity :

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

চিত্র ১। ম্যাটেরিয়াল তালিকা-বিবরণ দৃশ্যে চলমান বিষয়বস্তুর নমুনা।