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

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
  • metadata: Map<String, Any>: यह NavDisplay जैसे अन्य लाइब्रेरी कॉम्पोनेंट को सीन के बारे में जानकारी देता है. डिफ़ॉल्ट रूप से, यह फ़ंक्शन entries में मौजूद आखिरी NavEntry का metadata दिखाता है.

सीन की रणनीतियों के बारे में जानकारी

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 कॉलबैक.

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

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

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

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

  • जब बैक स्टैक में कुंजियां जोड़ी या हटाई जाती हैं, तब NavDisplay इन बदलावों को ट्रैक करता है. जैसे, backStack.add() या backStack.removeLastOrNull() का इस्तेमाल करना.
  • 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> {
    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.
 *
 */
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)
        }
    }
}

इस ListDetailSceneStrategy का इस्तेमाल अपने NavDisplay में करने के लिए, अपने entryProvider कॉल में बदलाव करें. इसमें ListDetailScene.listPane() मेटाडेटा को शामिल करें. यह मेटाडेटा उस एंट्री के लिए होना चाहिए जिसे आपको सूची लेआउट के तौर पर दिखाना है. साथ ही, ListDetailScene.detailPane() मेटाडेटा उस एंट्री के लिए होना चाहिए जिसे आपको जानकारी लेआउट के तौर पर दिखाना है. इसके बाद, 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)
}

अगर आपको खुद का लिस्ट-डिटेल सीन नहीं बनाना है, तो Material लिस्ट-डिटेल सीन का इस्तेमाल किया जा सकता है. इसमें काम की जानकारी और प्लेसहोल्डर के लिए सहायता मिलती है. इसके बारे में अगले सेक्शन में बताया गया है.

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

सूची की जानकारी वाले इस्तेमाल के उदाहरण के लिए, 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<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 में चल रहे कॉन्टेंट का उदाहरण.