सीन का इस्तेमाल करके कस्टम लेआउट बनाना

Navigation 3, Scenes के ज़रिए आपके ऐप्लिकेशन के यूज़र इंटरफ़ेस (यूआई) फ़्लो को मैनेज करने के लिए, एक बेहतरीन और फ़्लेक्सिबल सिस्टम उपलब्ध कराता है. सीन की मदद से, अपनी पसंद के मुताबिक लेआउट बनाए जा सकते हैं. साथ ही, इन्हें अलग-अलग स्क्रीन साइज़ के हिसाब से अडजस्ट किया जा सकता है. इसके अलावा, मल्टी-पैन के जटिल अनुभव को आसानी से मैनेज किया जा सकता है.

सीन के बारे में जानकारी

Navigation 3 में, Scene एक बुनियादी यूनिट होती है. यह एक या उससे ज़्यादा NavEntry इंस्टेंस रेंडर करती है. Scene को अपने यूज़र इंटरफ़ेस (यूआई) के एक अलग विज़ुअल स्टेट या सेक्शन के तौर पर समझें. इसमें आपके बैकस्टैक का कॉन्टेंट शामिल हो सकता है और उसे मैनेज किया जा सकता है.

हर Scene इंस्टेंस की पहचान, उसके key और Scene की क्लास से की जाती है. यह यूनीक आइडेंटिफ़ायर ज़रूरी है, क्योंकि Scene बदलने पर यह टॉप-लेवल ऐनिमेशन को ट्रिगर करता है.

Scene इंटरफ़ेस में ये प्रॉपर्टी होती हैं:

  • key: Any: यह Scene इंस्टेंस के लिए यूनीक आइडेंटिफ़ायर होता है. इस कुंजी और Scene की क्लास का इस्तेमाल करके, यह पक्का किया जाता है कि हर एलिमेंट अलग हो. ऐसा खास तौर पर ऐनिमेशन के लिए किया जाता है.
  • entries: List<NavEntry<T>>: यह NavEntry ऑब्जेक्ट की सूची है, जिसे दिखाने की ज़िम्मेदारी Scene की है. अहम बात यह है कि अगर ट्रांज़िशन के दौरान एक ही NavEntry को कई Scenes में दिखाया जाता है (जैसे, शेयर किए गए एलिमेंट ट्रांज़िशन में), तो इसका कॉन्टेंट सिर्फ़ उस सबसे नए टारगेट Scene से रेंडर किया जाएगा जो इसे दिखा रहा है.
  • previousEntries: List<NavEntry<T>>: इस प्रॉपर्टी से उन NavEntrys के बारे में पता चलता है जो मौजूदा Scene से "बैक" कार्रवाई होने पर दिखेंगे. यह सही प्रेडिक्टिव बैक स्टेट का हिसाब लगाने के लिए ज़रूरी है. इससे NavDisplay को पिछली सही स्थिति का अनुमान लगाने और उस पर ट्रांज़िशन करने की अनुमति मिलती है. यह किसी दूसरी क्लास और/या कुंजी वाला सीन हो सकता है.
  • content: @Composable () -> Unit: यह कंपोज़ेबल फ़ंक्शन है. इसमें यह तय किया जाता है कि Scene, अपने entries और उसके आस-पास मौजूद यूज़र इंटरफ़ेस (यूआई) के एलिमेंट को कैसे रेंडर करेगा.Scene

सीन की रणनीतियों को समझना

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 वैल्यू दिखानी चाहिए.

SceneStrategyScope, SceneStrategy के लिए ज़रूरी किसी भी वैकल्पिक तर्क को बनाए रखने के लिए ज़िम्मेदार होता है. जैसे, onBack कॉलबैक.

SceneStrategy में then इनफ़िक्स फ़ंक्शन भी उपलब्ध है. इसकी मदद से, कई रणनीतियों को एक साथ इस्तेमाल किया जा सकता है. इससे फ़ैसले लेने की एक फ़्लेक्सिबल पाइपलाइन बनती है. इसमें हर रणनीति, Scene का हिसाब लगाने की कोशिश कर सकती है. अगर ऐसा नहीं हो पाता है, तो यह चेन में मौजूद अगली रणनीति को काम सौंप देती है.

सीन और सीन की रणनीतियां एक साथ कैसे काम करती हैं

NavDisplay एक सेंट्रल कंपोज़ेबल है. यह आपके बैक स्टैक पर नज़र रखता है और सही Scene का पता लगाने और उसे रेंडर करने के लिए SceneStrategy का इस्तेमाल करता है.

NavDisplay's sceneStrategy पैरामीटर को SceneStrategy की ज़रूरत होती है. यह SceneStrategy, Scene को दिखाने के लिए कैलकुलेट करता है. अगर दी गई रणनीति (या रणनीतियों की चेन) से कोई Scene नहीं मिलता है, तो NavDisplay डिफ़ॉल्ट रूप से SinglePaneSceneStrategy का इस्तेमाल करने लगता है.

यहां इंटरैक्शन के बारे में बताया गया है:

  • जब बैक स्टैक में कुंजियां जोड़ी या हटाई जाती हैं (जैसे, backStack.add() या backStack.removeLastOrNull() का इस्तेमाल करके), तो NavDisplay इन बदलावों को ट्रैक करता है.
  • NavDisplay, कॉन्फ़िगर किए गए SceneStrategy's calculateScene तरीके को NavEntrys की मौजूदा सूची (बैकस्टैक कुंजियों से मिली) पास करता है.
  • अगर 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> {
    @Composable
    override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> =
        SinglePaneScene(
            key = entries.last().contentKey,
            entry = entries.last(),
            previousEntries = entries.dropLast(1)
        )
}

उदाहरण: दो पैन वाला बुनियादी लेआउट (कस्टम सीन और रणनीति)

इस उदाहरण में, दो पैनल वाला सामान्य लेआउट बनाने का तरीका बताया गया है. यह लेआउट इन दो शर्तों के आधार पर चालू होता है:

  1. विंडो की चौड़ाई इतनी होनी चाहिए कि दो पैनल (यानी कि कम से कम WIDTH_DP_MEDIUM_LOWER_BOUND) दिख सकें.
  2. बैक स्टैक में मौजूद पहले दो आइटम, खास मेटाडेटा का इस्तेमाल करके दो पैन वाले लेआउट में दिखाए जाने के लिए, साफ़ तौर पर अपनी सहमति देते हैं.

यहां दिया गया स्निपेट, TwoPaneScene.kt और TwoPaneSceneStrategy.kt के लिए, एक साथ इस्तेमाल किया जाने वाला सोर्स कोड है:

// --- TwoPaneScene ---
/**
 * A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split.
 */
class TwoPaneScene<T : Any>(
    override val key: Any,
    override val previousEntries: List<NavEntry<T>>,
    val firstEntry: NavEntry<T>,
    val secondEntry: NavEntry<T>
) : Scene<T> {
    override val entries: List<NavEntry<T>> = listOf(firstEntry, secondEntry)
    override val content: @Composable (() -> Unit) = {
        Row(modifier = Modifier.fillMaxSize()) {
            Column(modifier = Modifier.weight(0.5f)) {
                firstEntry.Content()
            }
            Column(modifier = Modifier.weight(0.5f)) {
                secondEntry.Content()
            }
        }
    }

    companion object {
        internal const val TWO_PANE_KEY = "TwoPane"
        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * in a two-pane layout.
         */
        fun twoPane() = mapOf(TWO_PANE_KEY to true)
    }
}

// --- TwoPaneSceneStrategy ---
/**
 * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough
 * and the top two back stack entries declare support for two-pane display.
 */
class TwoPaneSceneStrategy<T : Any> : SceneStrategy<T> {
    @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class)
    @Composable
    override fun calculateScene(
        entries: List<NavEntry<T>>,
        onBack: (Int) -> Unit
    ): Scene<T>? {

        val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass

        // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes.
        // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp).
        if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
            return null
        }

        val lastTwoEntries = entries.takeLast(2)

        // Condition 2: Only return a Scene if there are two entries, and both have declared
        // they can be displayed in a two pane scene.
        return if (lastTwoEntries.size == 2 &&
            lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) }
        ) {
            val firstEntry = lastTwoEntries.first()
            val secondEntry = lastTwoEntries.last()

            // The scene key must uniquely represent the state of the scene.
            val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey)

            TwoPaneScene(
                key = sceneKey,
                // Where we go back to is a UX decision. In this case, we only remove the top
                // entry from the back stack, despite displaying two entries in this scene.
                // This is because in this app we only ever add one entry to the
                // back stack at a time. It would therefore be confusing to the user to add one
                // when navigating forward, but remove two when navigating back.
                previousEntries = entries.dropLast(1),
                firstEntry = firstEntry,
                secondEntry = secondEntry
            )
        } else {
            null
        }
    }
}

अगर आपको इस TwoPaneSceneStrategy का इस्तेमाल अपने NavDisplay में करना है, तो अपने entryProvider कॉल में बदलाव करें. ऐसा करके, उन एंट्री के लिए TwoPaneScene.twoPane() मेटाडेटा शामिल करें जिन्हें आपको दो-पैन लेआउट में दिखाना है. इसके बाद, sceneStrategy के तौर पर TwoPaneSceneStrategy() दें. इससे सिंगल-पैन वाले मामलों में, डिफ़ॉल्ट फ़ॉलबैक पर भरोसा किया जा सकेगा:

// Define your navigation keys
@Serializable
data object ProductList : NavKey
@Serializable
data class ProductDetail(val id: String) : NavKey

@Composable
fun MyAppContent() {
    val backStack = rememberNavBackStack(ProductList)

    NavDisplay(
        backStack = backStack,
        entryProvider = entryProvider {
            entry<ProductList>(
                // Mark this entry as eligible for two-pane display
                metadata = TwoPaneScene.twoPane()
            ) { key ->
                Column {
                    Text("Product List")
                    Button(onClick = { backStack.add(ProductDetail("ABC")) }) {
                        Text("View Details for ABC (Two-Pane Eligible)")
                    }
                }
            }

            entry<ProductDetail>(
                // Mark this entry as eligible for two-pane display
                metadata = TwoPaneScene.twoPane()
            ) { key ->
                Text("Product Detail: ${key.id} (Two-Pane Eligible)")
            }
            // ... other entries ...
        },
        // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically.
        sceneStrategy = TwoPaneSceneStrategy<Any>(),
        onBack = { count ->
            repeat(count) {
                if (backStack.isNotEmpty()) {
                    backStack.removeLastOrNull()
                }
            }
        }
    )
}

मटेरियल अडैप्टिव सीन में सूची और जानकारी वाला कॉन्टेंट दिखाना

सूची-जानकारी के इस्तेमाल के उदाहरण के लिए, androidx.compose.material3.adaptive:adaptive-navigation3 आर्टफ़ैक्ट एक ListDetailSceneStrategy उपलब्ध कराता है, जो सूची-जानकारी वाला Scene बनाता है. यह Scene, एक से ज़्यादा पैन वाले जटिल लेआउट (सूची, जानकारी, और अतिरिक्त पैन) को अपने-आप मैनेज करता है. साथ ही, विंडो के साइज़ और डिवाइस की स्थिति के हिसाब से उन्हें अडजस्ट करता है.

मटेरियल लिस्ट-डिटेल Scene बनाने के लिए, यह तरीका अपनाएं:

  1. डिपेंडेंसी जोड़ना: अपने प्रोजेक्ट की build.gradle.kts फ़ाइल में androidx.compose.material3.adaptive:adaptive-navigation3 शामिल करें.
  2. ListDetailSceneStrategy मेटाडेटा की मदद से अपनी एंट्री तय करें: listPane(), detailPane() और extraPane() का इस्तेमाल करके, NavEntrys को मार्क करें, ताकि उसे सही पैनल में दिखाया जा सके. 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<Any>()

                NavDisplay(
                    backStack = backStack,
                    modifier = Modifier.padding(paddingValues),
                    onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } },
                    sceneStrategy = 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 में चल रहे कॉन्टेंट का उदाहरण.