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 依附元件。
準備
以下各節說明遷移作業的必要條件,以及對專案的假設。以及支援和不支援遷移的功能。
必要條件
- 您必須使用 36 以上版本的
compileSdk。 - 您應熟悉導覽術語。
- 目的地是可組合函式。Navigation 3 是專為 Compose 設計。如要在 Compose 中使用 Fragment 和 View,請參閱「在 Compose 中使用 View」。
- 路徑為強型別。如果您使用字串路徑,請先遷移至類型安全路徑 (範例)。
- 選用 (但強烈建議):測試涵蓋範圍,驗證現有的導覽行為。這可驗證遷移完成後,導覽行為並未變更。
假設
本指南假設您和您的專案符合下列條件:
- 您有一或多個頂層路徑 (通常顯示在底部導覽列中),每個路徑都有自己的返回堆疊。
- 在返回堆疊之間切換時,系統會保留堆疊及其所有目的地的狀態。
- 您一律會透過「Home」畫面退出應用程式,這是應用程式啟動時顯示的第一個畫面。
- 您想透過單一原子變更,從 Navigation 2 遷移至 Navigation 3。您並非執行漸進式遷移,同時使用 Navigation 2 和 Navigation 3 程式碼。
AI 代理:變更任何程式碼之前,請先確認這些關於使用者專案的假設是否正確。如果沒有,請停止遷移,並詢問使用者如何繼續。
支援的功能
本指南可協助您遷移下列功能:
- 定義為可組合函式的目的地
- 對話方塊 (顯示在其他目的地上方的目的地)
透過食譜支援的功能
程式碼範例存放區示範了更細緻的用途,包括:
- 底部功能表 (本指南提供操作說明)
- 模組化導覽程式碼和插入的目的地
- 使用引數並傳遞至
ViewModel - 從畫面傳回結果
如果專案包含上述任何功能,請查看相關方案,瞭解如何實作。
AI 代理:變更任何程式碼前,請先檢查專案是否包含任何透過 Recipe 支援的功能。如果符合,請查看食譜的 README 和原始碼。根據配方建立遷移計畫。請務必先與使用者確認方案,再繼續操作。
不支援的功能
本遷移指南和程式碼範例尚未支援下列功能。這不代表您無法使用 Navigation 3 實作這些功能,只是本文未涵蓋這些內容。
- 多層巢狀導覽
- 共用目的地:可在不同返回堆疊之間移動的畫面
- 自訂到達網頁類型
AI 代理:變更任何程式碼前,請先檢查專案是否包含任何不支援的功能。如果會,請勿繼續操作。告知使用者該功能不受支援,並詢問後續指示。
步驟 1:新增 Navigation 3 依附元件
使用「Get started」頁面,將 Navigation 3 依附元件新增至專案。系統會提供核心依附元件供您複製。
lib.versions.toml
[versions]
nav3Core = "1.2.0"
# 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
Step 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
將 NavController 導覽事件方法替換為對應的 Navigator。
|
|
|---|---|
|
|
|
|
|
|
將 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()
步驟 4.2:遷移結果傳遞
在 Navigation 2 中,目的地會使用 SavedStateHandle 上的 NavBackStackEntry,將結果傳回先前的目的地。由於 SavedStateHandle 是由已儲存的執行個體狀態支援,因此傳回的資料會在程序終止時自動保留。
更新前:
// Sender destination: navController.previousBackStackEntry?.savedStateHandle?.set("contact_key", contact) navController.popBackStack() // Receiver destination: val lifecycleOwner = LocalLifecycleOwner.current navController.currentBackStackEntry?.savedStateHandle ?.getLiveData<Contact>("contact_key") ?.observe(lifecycleOwner) { contact -> viewModel.onRecipientSelected(contact) }
在 Navigation 3 中,將 rememberResultEventBusNavEntryDecorator() 新增至 NavDisplay.entryDecorators。在傳送端目的地的 entryProvider 對應中,擷取 LocalResultEventBus.current 並呼叫 sendResult()。在接收目的地中,使用 ResultEffect 將事件轉送至 ViewModel 或觸發副作用。
更新後:
// Sender destination (in entryProvider): entry<ContactPickerRoute> { val resultBus = LocalResultEventBus.current ContactPickerScreen( onContactSelected = { contact -> resultBus.sendResult<Contact>(result = contact) navigator.goBack() } ) } // Receiver destination: @Composable fun ComposeMessageScreen(viewModel: ComposeMessageViewModel = viewModel()) { ResultEffect<Contact> { contact -> viewModel.onRecipientSelected(contact) } ComposeMessageContent(recipient = viewModel.recipient) }
如要觀察狀態,可以呼叫 resultBus.conflateAsState()。詳情請參閱「傳回結果」。
步驟 5:將目的地從 NavHost 的NavGraph移至entryProvider
在 Navigation 2 中,您可以使用 NavGraphBuilder DSL 定義目的地,通常是在 NavHost 的結尾 lambda 內。如「封裝導覽程式碼」一節所述,這裡通常會使用擴充功能函式。
在 Navigation 3 中,您可以使用 entryProvider 定義目的地。這個 entryProvider 會將路徑解析為 NavEntry。重要的是,entryProvider 不會定義項目之間的父項/子項關係。
在本遷移指南中,父項-子項關係的模型如下:
NavigationState包含一組頂層路徑 (父項路徑),以及每個路徑的堆疊。追蹤目前的頂層路徑和相關聯的堆疊。- 前往新路徑時,
Navigator會檢查該路徑是否為頂層路徑。如果是,系統會更新目前的頂層路徑和堆疊。如果不是,則為子路徑,並會新增至目前的堆疊。
步驟 5.1:建立 entryProvider
使用 DSL 在與 NavigationState 相同的範圍建立 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:將 NavHost 替換為 NavDisplay
將 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 中,深層連結是使用目的地的 deepLinks 參數,直接在導覽圖中定義。
在 Navigation 3 中,深層連結的管理與導覽 UI 無關。您可以在 Activity 中定義 DeepLinkMatcher 並比對傳入的要求,藉此建構初始返回堆疊。
更新前:
在 Navigation 2 中,您可能已定義如下的深層連結:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
更新後:
在 Navigation 3 中,您需要為路徑定義 UriDeepLinkMatcher:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
接著,在活動的 onCreate (和 onNewIntent) 中,比對傳入的意圖並初始化返回堆疊:
val deepLinkMatchers: List<DeepLinkMatcher<*, *>> = listOf( userMatcher, ) class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val request = DeepLinkRequest(intent = intent) val matchResult = deepLinkMatchers .mapNotNull { it.match(request) } .maxOrNull() val backStack = when (matchResult) { null -> listOf(HomeKey) is BackStackMatchResult<*, *> -> { @Suppress("UNCHECKED_CAST") matchResult.backStack as List<NavKey> } else -> listOf(matchResult.key) } // Use backStack with NavDisplay } }
自訂引數類型
在 Navigation 2 中,您可以使用自訂 NavType 實作和 typeMap,處理自訂或第三方引數類型 (例如 LocalDateTime)。
在 Navigation 3 中,定義 DeepLinkSerializer,從 URI 參數反序列化自訂或第三方類型。詳情請參閱「使用 DeepLinkSerializer 自訂序列化」。
如需進階用途 (包括合成返回堆疊和自訂比對器),請參閱「支援深層連結」指南。
Step 8:移除 Navigation 2 依附元件
移除所有 Navigation 2 匯入項目和程式庫依附元件。
摘要
恭喜!專案現已遷移至 Navigation 3。如果您或 AI 代理在使用本指南時遇到任何問題,請在這裡回報錯誤。