如要將應用程式從 Navigation 2 遷移至 Navigation 3,請按照下列步驟操作:
- 新增 Navigation 3 依附元件。
- 更新導覽路徑,實作
NavKey介面。 - 建立類別來保留及修改導覽狀態。
- 將
NavController替換為這些類別。 - 將目的地從
NavHost的NavGraph移至entryProvider。 - 將
NavHost替換為NavDisplay。 - 移除 Navigation 2 依附元件。
使用 AI 代理
您可以搭配 AI 代理程式使用本指南,例如 Android Studio 的 Agent 模式中的 Gemini。本指南中以「AI 代理:」開頭的行應由 AI 代理讀取,但人類讀者可以忽略。
準備
以下各節說明遷移作業的先決條件,以及專案的假設條件。以及支援和不支援的遷移功能。
必要條件
- 您必須使用
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.0.0"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"
[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 的參照,包括任何匯入項目。
步驟 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 {
}
步驟 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
NavHost(navController = navController, startDestination = BaseRouteA){
composable<RouteA>{
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:將 NavHost 替換為 NavDisplay
將 NavHost 替換為 NavDisplay。
- 刪除
NavHost,並改為輸入NavDisplay。 - 將
entries = navigationState.toEntries(entryProvider)指定為參數。 這會使用entryProvider,將導覽狀態轉換為NavDisplay顯示的項目。 - 將「
NavDisplay.onBack」連結至「navigator.goBack()」。這會導致navigator在NavDisplay的內建返回處理常式完成時更新導覽狀態。 - 如果您有對話目的地,請將
DialogSceneStrategy新增至NavDisplay的sceneStrategy參數。
例如:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategy = remember { DialogSceneStrategy() }
)
步驟 7:移除 Navigation 2 依附元件
移除所有 Navigation 2 匯入項目和程式庫依附元件。
摘要
恭喜!您的專案現已遷移至 Navigation 3。如果您或 AI 代理程式在使用本指南時遇到任何問題,請在這裡回報錯誤。