從 Navigation 2 遷移至 Navigation 3

如要將應用程式從 Navigation 2 遷移至 Navigation 3,請按照下列步驟操作:

  1. 新增 Navigation 3 依附元件。
  2. 更新導覽路徑,實作 NavKey 介面。
  3. 建立類別來保留及修改導覽狀態。
  4. NavController 替換為這些類別。
  5. 將目的地從 NavHostNavGraph 移至 entryProvider
  6. NavHost 替換為 NavDisplay
  7. 移除 Navigation 2 依附元件。

使用 AI 代理

您可以搭配 AI 代理程式使用本指南,例如 Android Studio 的 Agent 模式中的 Gemini。本指南中以「AI 代理:」開頭的行應由 AI 代理讀取,但人類讀者可以忽略。

準備

以下各節說明遷移作業的先決條件,以及專案的假設條件。以及支援和不支援的遷移功能。

必要條件

  • 您必須使用 compileSdk 36 以上版本。
  • 您應熟悉導覽術語
  • 目的地是可組合函式。Navigation 3 專為 Compose 設計。如要在 Compose 中使用 Fragment 和 View,請參閱「在 Compose 中使用 View」。
  • 路徑是強型別。如果您使用以字串為基礎的路徑,請先遷移至類型安全的路徑 (範例)。
  • 選用 (但強烈建議):測試涵蓋範圍,驗證現有的導覽行為。這項檢查可確保遷移完成後,導覽行為不會改變。

假設

本指南會對您和您的專案做出下列假設:

  • 您有多個頂層路徑 (通常顯示在底部導覽列中),每個路徑都有自己的返回堆疊。
  • 切換返回堆疊時,系統會保留堆疊的狀態和所有目的地。
  • 您一律透過「首頁」畫面結束應用程式,這個畫面是應用程式啟動時顯示的第一個畫面。
  • 您想以單一原子變更,從 Navigation 2 遷移至 Navigation 3。您並未執行漸進式遷移,也就是同時使用 Navigation 2 和 Navigation 3 程式碼。

AI 代理程式:變更任何程式碼前,請先確認您對使用者專案的這些假設是否正確。如果不是,請停止遷移作業,並詢問使用者如何繼續。

支援的功能

本指南可協助您遷移下列功能:

  • 定義為可組合函式的目的地
  • 對話方塊 (顯示在另一個目的地頂端的目的地)

透過食譜支援的功能

程式碼範例存放區會展示更多細微的使用情境,包括:

如果專案包含上述任一功能,請參閱相關食譜,瞭解如何實作這些功能。

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.ktslib.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:建立 NavigationStateNavigator

建立 NavigationStateNavigator 的例項,範圍與 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 欄位或方法

Navigator 同等

navigate()

navigate()

popBackStack()

goBack()

NavController 欄位替換為 NavigationState 欄位。

NavController 欄位或方法

NavigationState 同等

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

取得頂層路徑:從目前的返回堆疊項目向上遍歷階層,即可找到頂層路徑。

topLevelRoute

使用 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:將目的地從 NavHostNavGraph 移至 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()」。這會導致 navigatorNavDisplay 的內建返回處理常式完成時更新導覽狀態。
  • 如果您有對話目的地,請將 DialogSceneStrategy 新增至 NavDisplaysceneStrategy 參數。

例如:

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategy = remember { DialogSceneStrategy() }
)

步驟 7:移除 Navigation 2 依附元件

移除所有 Navigation 2 匯入項目和程式庫依附元件。

摘要

恭喜!您的專案現已遷移至 Navigation 3。如果您或 AI 代理程式在使用本指南時遇到任何問題,請在這裡回報錯誤