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物件清單。重要的是,如果在轉換期間 (例如在共用元素轉換中) 多個Scenes顯示相同的NavEntry,則其內容只會由顯示該內容的最新目標Scene轉譯。previousEntries: List<NavEntry<T>>:這項屬性定義了從目前Scene執行「返回」動作時,會產生的NavEntry。這對於計算正確的預測返回狀態至關重要,可讓NavDisplay預測並轉換至正確的上一個狀態,這可能是具有不同類別、索引鍵或兩者的 Scene。content: @Composable () -> Unit:這個可組合函式用於定義Scene如何算繪其entries,以及該Scene專屬的任何周圍 UI 元素。metadata: Map<String, Any>:向其他程式庫元件 (例如NavDisplay) 提供場景專屬資訊。根據預設,會傳回entries中最後一個NavEntry的metadata。
在自訂場景中實作 equals 和 hashCode
自訂 Scene 實作項目必須正確實作 equals 和 hashCode,才能支援下列項目:
- 場景狀態轉換:每次場景策略清單計算場景時,
NavDisplay都會檢查新場景和前一個場景是否相同。如果偵測到變更,就會更新用於在場景之間轉換的SeekableTransition,進而影響場景的生命週期狀態。 - 疊加層管理:對於
OverlayScene(例如對話方塊),NavDisplay會使用場景物件本身做為key,追蹤疊加層生命週期和退出動畫。
確認定義場景內容的所有屬性 (例如 key、entries 和 previousEntries) 都包含在 equals 和 hashCode 實作中。一般來說,請避免在這些方法的實作中加入回呼 (例如 onBack),因為這類回呼可能會變更例項,但不會變更邏輯場景狀態。
瞭解場景策略
SceneStrategy 是一種機制,可決定如何安排返回堆疊中的指定 NavEntry 清單,並將其轉換為 Scene。基本上,當系統顯示目前的返回堆疊項目時,SceneStrategy 會問自己兩個重要問題:
- 我可以根據這些項目建立
Scene嗎?如果SceneStrategy判斷可以處理指定的NavEntry並形成有意義的Scene(例如對話方塊或多窗格版面配置),就會繼續執行。否則,則會傳回null,讓其他策略有機會建立Scene。 - 如果是,我該如何將這些項目排列到
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 是中央可組合項,可觀察返回堆疊,並使用一或多個 SceneStrategy 判斷及算繪適當的 Scene。
NavDisplay 的 sceneStrategies 參數需要 SceneStrategy 執行個體清單,負責計算要顯示的 Scene。如果提供的策略未計算出 Scene,NavDisplay 預設會自動改用 SinglePaneSceneStrategy。
以下詳細說明互動過程:
- 當您在返回堆疊中新增或移除鍵時 (例如使用
backStack.add()或backStack.removeLastOrNull()),NavDisplay會觀察這些變更。 NavDisplay會將目前的NavEntry清單 (衍生自返回堆疊索引鍵) 依序傳遞至設定的sceneStrategies,並在每個calculateScene上呼叫,直到傳回Scene為止。- 當
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) ) }
範例:基本清單/詳細資料版面配置 (自訂 Scene 和策略)
以下範例說明如何建立清單詳細資料版面配置,並根據兩個條件啟用:
- 視窗寬度足以支援兩個窗格 (即至少
WIDTH_DP_MEDIUM_LOWER_BOUND)。 - 返回堆疊包含的項目已宣告支援使用特定中繼資料,在清單詳細資料版面配置中顯示。
以下程式碼片段是 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,請修改 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 會自動處理複雜的多窗格排列 (清單、詳細資料和額外窗格),並根據視窗大小和裝置狀態進行調整。
如要建立 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<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") } } ) } } } }