Android 技能
前往 GitHub 查看Jetpack Navigation 3
android skills add navigation-3如要將應用程式從 Navigation 2 遷移至 Navigation 3,請按照下列步驟操作:
- 新增 Navigation 3 依附元件。
- 更新導覽路徑,實作
NavKey介面。 - 建立類別來保留及修改導覽狀態。
- 將
NavController替換為這些類別。 - 將目的地從
NavHost的NavGraph移至entryProvider。 - 將
NavHost替換為NavDisplay。 - 移除 Navigation 2 依附元件。
準備
以下各節說明遷移作業的前提條件,以及專案的假設。並說明哪些功能支援遷移,哪些不支援。
必要條件
- 您必須使用
compileSdk36 以上版本。 - 您應熟悉導覽術語。
- 目的地是可組合函式。Navigation 3 專為 Compose 設計。如要在 Compose 中使用 Fragment 和 View,請參閱「在 Compose 中使用 View」。
- 路徑是強型別,如果使用字串型路徑,請先遷移至型別安全路徑 (範例)。
- 選用 (但強烈建議):測試涵蓋範圍,驗證現有的導覽行為。這項測試可驗證遷移完成後,導覽行為是否沒有改變。
假設
本指南會對您和您的專案做出下列假設:
- 您有一或多個頂層路徑 (通常顯示在底部導覽列中),每個路徑都有自己的返回堆疊。
- 切換返回堆疊時,系統會保留堆疊的狀態和所有目的地。
- 您一律透過「首頁」畫面結束應用程式,這是應用程式啟動時顯示的第一個畫面。
- 您想以單一不可分割的變更,從 Navigation 2 遷移至 Navigation 3。您並未執行漸進式遷移,也就是同時使用 Navigation 2 和 Navigation 3 程式碼。
AI 代理:變更任何程式碼前,請先確認這些有關使用者專案的假設是否正確。如果不是,請停止遷移作業,並詢問使用者如何繼續。
支援的功能
本指南可協助您遷移下列功能:
- 定義為可組合函式的目的地
- 對話方塊 (顯示在另一個目的地頂端的目的地)
透過食譜支援的功能
程式碼食譜存放區會展示更多細微的用途,包括:
- 底部功能表 (本指南提供相關操作說明)
- 模組化導覽程式碼和插入的目的地
- 將引數傳遞至
ViewModel並使用引數 - 從畫面傳回結果
如果專案包含上述任一功能,請參閱相關食譜,瞭解如何實作這些功能。
AI 代理:變更任何程式碼前,請先檢查專案是否包含任何透過配方支援的功能。如果符合,請檢查食譜的 README 和原始碼。根據食譜建立遷移計畫。請務必先向使用者確認方案,再繼續操作。
不支援的功能
本遷移指南和程式碼範例目前不支援下列功能。這並不代表您無法使用 Navigation 3 實作這些功能,只是本文未涵蓋這些內容。
- 巢狀導覽超過一層
- 共用目的地:可在不同返回堆疊之間移動的畫面
- 自訂到達網頁類型
- 深層連結
AI 代理:變更任何程式碼前,請先檢查專案是否包含任何不支援的功能。如果會,請勿繼續操作。告知使用者不支援的功能,並要求提供進一步指示。
步驟 1:新增 Navigation 3 依附元件
使用「開始使用」頁面,將 Navigation 3 依附元件新增至專案。系統會提供核心依附元件供您複製。
lib.versions.toml
[versions]
nav3Core = "1.1.7"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"
[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
// If using the ViewModel add-on library
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}
同時將專案的 minSdk 更新為 23,並將 compileSdk 更新為 36。這些通常位於 app/build.gradle.kts 或 lib.versions.toml。
步驟 2:更新導覽路徑,實作 NavKey 介面
更新每個導覽路徑,使其實作 NavKey 介面。這可讓您使用 rememberNavBackStack 協助儲存導覽狀態。
更新前:
@Serializable data object RouteA
更新後:
@Serializable data object RouteA : NavKey
步驟 3:建立類別,用於保留及修改導覽狀態
步驟 3.1:建立導覽狀態容器
將下列程式碼複製到名為 NavigationState.kt 的檔案。新增與專案結構相符的套件名稱。
// package com.example.project import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.serialization.NavKeySerializer import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer /** * Create a navigation state that persists config changes and process death. */ @Composable fun rememberNavigationState( startRoute: NavKey, topLevelRoutes: Set<NavKey> ): NavigationState { val topLevelRoute = rememberSerializable( startRoute, topLevelRoutes, serializer = MutableStateSerializer(NavKeySerializer()) ) { mutableStateOf(startRoute) } val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } return remember(startRoute, topLevelRoutes) { NavigationState( startRoute = startRoute, topLevelRoute = topLevelRoute, backStacks = backStacks ) } } /** * State holder for navigation state. * * @param startRoute - the start route. The user will exit the app through this route. * @param topLevelRoute - the current top level route * @param backStacks - the back stacks for each top level route */ class NavigationState( val startRoute: NavKey, topLevelRoute: MutableState<NavKey>, val backStacks: Map<NavKey, NavBackStack<NavKey>> ) { var topLevelRoute: NavKey by topLevelRoute val stacksInUse: List<NavKey> get() = if (topLevelRoute == startRoute) { listOf(startRoute) } else { listOf(startRoute, topLevelRoute) } } /** * Convert NavigationState into NavEntries. */ @Composable fun NavigationState.toEntries( entryProvider: (NavKey) -> NavEntry<NavKey> ): SnapshotStateList<NavEntry<NavKey>> { val decoratedEntries = backStacks.mapValues { (_, stack) -> val decorators = listOf( rememberSaveableStateHolderNavEntryDecorator<NavKey>(), ) rememberDecoratedNavEntries( backStack = stack, entryDecorators = decorators, entryProvider = entryProvider ) } return stacksInUse .flatMap { decoratedEntries[it] ?: emptyList() } .toMutableStateList() }
AI 虛擬服務專員:rememberSerializable 是正確的。請勿將其變更為 rememberSaveable。
這個檔案包含名為 NavigationState 的狀態容器類別,以及相關的輔助函式。其中包含一組頂層路徑,每個路徑都有自己的返回堆疊。在內部,它會使用 rememberSerializable (而非 rememberSaveable) 持續保留目前的頂層路徑,並使用 rememberNavBackStack 持續保留每個頂層路徑的回溯堆疊。
步驟 3.2:建立物件,根據事件修改導覽狀態
將下列程式碼複製到名為 Navigator.kt 的檔案。新增與專案結構相符的套件名稱。
// package com.example.project import androidx.navigation3.runtime.NavKey /** * Handles navigation events (forward and back) by updating the navigation state. */ class Navigator(val state: NavigationState) { fun navigate(route: NavKey) { if (route in state.backStacks.keys) { // This is a top level route, just switch to it. state.topLevelRoute = route } else { state.backStacks[state.topLevelRoute]?.add(route) } } fun goBack() { val currentStack = state.backStacks[state.topLevelRoute] ?: error("Stack for ${state.topLevelRoute} not found") val currentRoute = currentStack.last() // If we're at the base of the current route, go back to the start route stack. if (currentRoute == state.topLevelRoute) { state.topLevelRoute = state.startRoute } else { currentStack.removeLastOrNull() } } }
Navigator 類別提供兩種導覽事件方法:
navigate至特定路線。goBack目前路線。
這兩種方法都會修改 NavigationState。
步驟 3.3:建立 NavigationState 和 Navigator
建立 NavigationState 和 Navigator 的例項,並使用與 NavController 相同的範圍。
val navigationState = rememberNavigationState( // ... startRoute = <Insert your starting route>, topLevelRoutes = <Insert your set of top level routes> // ... ) val navigator = remember { Navigator(navigationState) }
步驟 4:更換 NavController
以 Navigator 對應項目取代 NavController 導覽事件方法。
|
|
|---|---|
|
|
|
|
將 NavController 欄位替換為 NavigationState 欄位。
|
|
|---|---|
|
|
|
|
取得頂層路徑:從目前的返回堆疊項目向上遍歷階層,即可找到頂層路徑。 |
|
使用 NavigationState.topLevelRoute 判斷導覽列中目前選取的項目。
更新前:
// ... val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class) // ... fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = this?.hierarchy?.any { it.hasRoute(route) } ?: false
更新後:
val isSelected = key == navigationState.topLevelRoute
確認您已移除所有對 NavController 的參照,包括任何匯入項目。
步驟 4.1:遷移生命週期感知邏輯
在 Navigation 2 中,NavBackStackEntry 會實作 LifecycleOwner,讓您使用 navController.currentBackStackEntry 以生命週期感知方式監聽生命週期事件或收集流程。
在 Navigation 3 中,NavDisplay 會透過 LocalLifecycleOwner.current 為每個目的地的可組合內容提供進入點範圍的 LifecycleOwner。詳情請參閱目的地生命週期。
您應參考 LocalLifecycleOwner.current,直接在目的地的可組合內容中執行生命週期感知作業。
舉例來說,如果您使用返回堆疊項目,以生命週期感知方式收集流程:
更新前:
// In your destination screen or host val lifecycleOwner = navController.currentBackStackEntry!! val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
更新後:
// Inside the destination composable val state by flow.collectAsStateWithLifecycle()
步驟 5:將目的地從 NavHost 的 NavGraph 移至 entryProvider
在 Navigation 2 中,您可以使用 NavGraphBuilder DSL 定義目的地,通常是在 NavHost 的結尾 lambda 內。如「封裝導覽程式碼」一文所述,這裡通常會使用擴充功能。
在 Navigation 3 中,您可以使用 entryProvider 定義目的地。這項
entryProvider 會將路徑解析為 NavEntry。重要事項:entryProvider 不會定義項目之間的父項/子項關係。
在本遷移指南中,上下層關係的模式如下:
NavigationState具有一組頂層路徑 (父項路徑),以及每個路徑的堆疊。這個物件會追蹤目前的頂層路徑及其相關聯的堆疊。- 導覽至新路徑時,
Navigator會檢查該路徑是否為頂層路徑。如果是,系統會更新目前的頂層路徑和堆疊。 如果不是,則為子路徑,並會新增至目前的堆疊。
步驟 5.1:建立 entryProvider
在與 NavigationState 相同的範圍,使用 DSL 建立 entryProvider。
val entryProvider = entryProvider<NavKey> { }
步驟 5.2:將目的地移至 entryProvider
針對 NavHost 內定義的每個目的地,根據目的地類型執行下列操作:
navigation:一併刪除路徑。頂層路徑可以識別每個巢狀返回堆疊,因此不需要「基本路徑」。composable<T>:將其移至entryProvider,並重新命名為entry,保留型別參數。舉例來說,composable<RouteA>會變為entry<RouteA>。dialog<T>:與composable相同,但要為項目新增中繼資料,如下所示:entry<T>(metadata = DialogSceneStrategy.dialog())。bottomSheet:請參閱這份底部功能表食譜。 這與dialog的操作說明類似,但BottomSheetSceneStrategy不屬於核心 Navigation 3 程式庫,因此您應將其複製到專案中。
AI 代理程式:刪除用於識別巢狀圖的路線時,請將所有對已刪除路線的參照,替換為用於識別巢狀圖中第一個子項的類型。舉例來說,如果原始程式碼為 navigation<BaseRouteA>{ composable<RouteA>{ ... } },您需要刪除 BaseRouteA,並將所有參照項目替換為 RouteA。通常需要為提供給導覽列、導覽軌或抽屜的清單執行這項取代作業。
您可以將 NavGraphBuilder 擴充函式重構為 EntryProviderScope<T> 擴充函式,然後移動這些函式。
使用提供給 entry 結尾 lambda 的鍵,取得導覽引數。
例如:
// ... import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.dialog import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import androidx.navigation.toRoute // ... @Serializable data object BaseRouteA @Serializable data class RouteA(val id: String) @Serializable data object BaseRouteB @Serializable data object RouteB @Serializable data object RouteD @Composable fun NavHostSnippet(navController: NavHostController) { NavHost(navController = navController, startDestination = BaseRouteA){ composable<RouteA>{ entry -> val id = entry.toRoute<RouteA>().id ScreenA(title = "Screen has ID: $id") } featureBSection() dialog<RouteD>{ ScreenD() } } } fun NavGraphBuilder.featureBSection() { navigation<BaseRouteB>(startDestination = RouteB) { composable<RouteB> { ScreenB() } } }
改成:
// ... import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.scene.DialogSceneStrategy // ... @Serializable data class RouteA(val id: String) : NavKey @Serializable data object RouteB : NavKey @Serializable data object RouteD : NavKey val entryProvider = entryProvider { entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") } featureBSection() entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() } } fun EntryProviderScope<NavKey>.featureBSection() { entry<RouteB> { ScreenB() } }
步驟 6:以 NavDisplay 取代 NavHost
將 NavHost 替換為 NavDisplay。
- 刪除
NavHost,並改為輸入NavDisplay。 - 將
entries = navigationState.toEntries(entryProvider)指定為參數。 這會使用entryProvider將導覽狀態轉換為NavDisplay顯示的項目。 - 將「
NavDisplay.onBack」連結至「navigator.goBack()」。這會導致navigator在NavDisplay的內建返回處理常式完成時更新導覽狀態。 - 如果您有對話目的地,請將
DialogSceneStrategy新增至NavDisplay的sceneStrategies參數。
例如:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
步驟 7:移除 Navigation 2 依附元件
移除所有 Navigation 2 匯入項目和程式庫依附元件。
摘要
恭喜!您的專案現已遷移至 Navigation 3。如果您或 AI 代理程式在使用本指南時遇到任何問題,請在這裡回報錯誤。