Navigation 3 推出了強大且靈活的系統,可透過場景管理應用程式的 UI 流程。您可以使用場景建立高度客製化的版面配置、配合不同螢幕大小調整版面配置,以及順暢地管理複雜的多窗格體驗。
瞭解場景
在 Navigation 3 中,Scene
是用於轉譯一或多個 NavEntry
例項的基本單位。您可以將 Scene
視為 UI 的獨特視覺狀態或區段,可包含及管理來自後端堆疊的內容顯示。
每個 Scene
例項都由其 key
和 Scene
本身的類別唯一識別。這個專屬 ID 非常重要,因為它會在 Scene
變更時驅動頂層動畫。
Scene
介面具有下列屬性:
key: Any
:這個特定Scene
例項的專屬 ID。這個鍵與Scene
類別搭配使用,可確保區別性,主要用於動畫效果。entries: List<NavEntry<T>>
:這是Scene
負責顯示的NavEntry
物件清單。重要的是,如果在轉場期間 (例如在共用元素轉場) 有相同的NavEntry
顯示在多個Scenes
中,則只有最近顯示該元素的目標Scene
才會算繪其內容。previousEntries: List<NavEntry<T>>
:這項屬性會定義在從目前Scene
發生「back」動作時,所產生的NavEntry
。這對於計算適當的預測返回狀態至關重要,讓NavDisplay
能夠預測並轉換至正確的上一個狀態,該狀態可能是具有不同類別和/或鍵的場景。content: @Composable () -> Unit
:這是可組合函式,可用於定義Scene
如何轉譯其entries
,以及任何特定於該Scene
的周圍 UI 元素。
瞭解場景策略
SceneStrategy
是決定如何安排和轉換後疊堆中特定 NavEntry
清單的機制。Scene
基本上,當 SceneStrategy
看到目前的返回堆疊項目時,會向自己詢問兩個重要問題:
- 我可以使用這些項目建立
Scene
嗎?如果SceneStrategy
判斷可以處理指定的NavEntry
,並形成有意義的Scene
(例如對話方塊或多窗格版面配置),就會繼續執行。否則,會傳回null
,讓其他策略有機會建立Scene
。 - 如果是這樣,我該如何將這些項目排入
Scene?
一旦SceneStrategy
承諾處理項目,就會負責建構Scene
,並定義如何在該Scene
中顯示指定的NavEntry
。
SceneStrategy
的核心是 calculateScene
方法:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
這個方法會從返回堆疊和 onBack
回呼中取得目前的 List<NavEntry<T>>
。如果能成功從提供的項目建立一個項目,則應傳回 Scene<T>
;如果無法建立,則應傳回 null
。
SceneStrategy
也提供方便的 then
中綴函式,可讓您將多個策略串連在一起。這會建立靈活的決策管道,讓每個策略都能嘗試計算 Scene
,如果無法計算,則會將權限委派給鏈結中的下一個策略。
場景和場景策略的搭配運作方式
NavDisplay
是觀察返回堆疊的核心可組合項,並使用 SceneStrategy
判斷並轉譯適當的 Scene
。
NavDisplay's sceneStrategy
參數會預期 SceneStrategy
,負責計算要顯示的 Scene
。如果提供的策略 (或策略鏈結) 未計算出 Scene
,NavDisplay
會自動改為預設使用 SinglePaneSceneStrategy
。
以下是互動情形的詳細資料:
- 當您在返回堆疊中新增或移除鍵 (例如使用
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: T, 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.invoke(entry.key) } } /** * 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().key, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
範例:基本雙格版面配置 (自訂場景和策略)
本範例說明如何建立簡單的雙窗格版面配置,並根據下列兩種條件啟用:
- 視窗寬度足以支援兩個窗格 (即至少
WIDTH_DP_MEDIUM_LOWER_BOUND
)。 - 返回堆疊中的前兩個項目明確宣告支援使用特定中繼資料,在雙窗格版面配置中顯示。
下列程式碼片段是 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.invoke(firstEntry.key) } Column(modifier = Modifier.weight(0.5f)) { secondEntry.content.invoke(secondEntry.key) } } } 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.key, secondEntry.key) 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 } } }
如要在 NavDisplay
中使用這個 TwoPaneSceneStrategy
,請修改 entryProvider
呼叫,為您想在雙窗格版面配置中顯示的項目加入 TwoPaneScene.twoPane()
中繼資料。接著,請將 TwoPaneSceneStrategy()
做為 sceneStrategy
,並在單一檢視畫面情境中依賴預設的備用方案:
// 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() } } } ) }
在 Material 自動調整式場景中顯示清單/詳細資料內容
針對清單詳細資料使用案例,androidx.compose.material3.adaptive:adaptive-navigation3
構件會提供 ListDetailSceneStrategy
,用於建立清單詳細資料 Scene
。這個 Scene
會自動處理複雜的多窗格排版 (清單、詳細資料和額外窗格),並根據視窗大小和裝置狀態進行調整。
如要建立 Material 清單/詳細資料 Scene
,請按照下列步驟操作:
- 新增依附元件:在專案的
build.gradle.kts
檔案中加入androidx.compose.material3.adaptive:adaptive-navigation3
。 - 使用
ListDetailSceneStrategy
中繼資料定義項目:使用listPane(), detailPane()
和extraPane()
標示NavEntrys
,以便在適當的窗格中顯示。listPane()
輔助程式還可讓您在未選取任何項目時指定detailPlaceholder
。 - 使用
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") } } ) } } } }