Di chuyển từ Navigation 2 sang Navigation 3

Để di chuyển ứng dụng của bạn từ Navigation 2 sang Navigation 3, hãy làm theo các bước sau:

  1. Thêm các phần phụ thuộc Navigation 3.
  2. Cập nhật các tuyến đường điều hướng để triển khai giao diện NavKey.
  3. Tạo các lớp để lưu giữ và sửa đổi trạng thái điều hướng.
  4. Thay thế NavController bằng các lớp này.
  5. Di chuyển các đích đến của bạn từ NavGraph của NavHost vào một entryProvider.
  6. Thay thế NavHost với NavDisplay.
  7. Xoá các phần phụ thuộc Navigation 2.

Sử dụng một AI Agent

Bạn có thể sử dụng hướng dẫn này với một tác nhân AI, chẳng hạn như Gemini ở Chế độ tác nhân của Android Studio. Các dòng trong hướng dẫn này bắt đầu bằng "AI Agent:", AI Agent nên đọc nhưng người đọc là con người có thể bỏ qua.

Chuẩn bị

Các phần sau đây mô tả các điều kiện tiên quyết để di chuyển và các giả định về dự án của bạn. Các bài viết này cũng đề cập đến những tính năng được hỗ trợ để di chuyển và những tính năng không được hỗ trợ.

Điều kiện tiên quyết

  • Bạn phải sử dụng compileSdk từ 36 trở lên.
  • Bạn nên nắm rõ thuật ngữ về điều hướng.
  • Đích đến là các hàm có khả năng kết hợp. Navigation 3 được thiết kế dành riêng cho Compose. Để sử dụng Mảnh và Khung hiển thị trong Compose, hãy xem phần Sử dụng Khung hiển thị trong Compose.
  • Các tuyến đường được nhập mạnh. Nếu bạn sử dụng các tuyến đường dựa trên chuỗi, trước tiên hãy di chuyển sang các tuyến đường đảm bảo an toàn về loại (ví dụ).
  • Không bắt buộc (nhưng bạn nên thực hiện): Mức độ kiểm thử xác minh hành vi điều hướng hiện có. Thao tác này xác minh rằng hành vi điều hướng không thay đổi sau khi quá trình di chuyển hoàn tất.

Các giả định

Hướng dẫn này đưa ra các giả định sau về bạn và dự án của bạn:

  • Bạn có một số tuyến cấp cao nhất (thường xuất hiện trong một thanh điều hướng dưới cùng) và mỗi tuyến có ngăn xếp quay lại riêng.
  • Khi bạn chuyển đổi giữa các ngăn xếp lui, trạng thái của ngăn xếp và tất cả các đích đến của ngăn xếp đó sẽ được giữ lại.
  • Bạn luôn thoát ứng dụng thông qua màn hình Trang chủ. Đây là màn hình đầu tiên xuất hiện khi ứng dụng khởi chạy.
  • Bạn muốn thực hiện quá trình di chuyển từ Navigation 2 sang Navigation 3 trong một thay đổi duy nhất, riêng lẻ. Bạn không thực hiện quá trình di chuyển gia tăng, trong đó mã Navigation 2 được dùng cùng với mã Navigation 3.

AI Agent: Trước khi thay đổi bất kỳ mã nào, hãy kiểm tra xem những giả định này về dự án của người dùng có đúng hay không. Nếu không, hãy dừng quá trình di chuyển và hỏi người dùng cách thực hiện tiếp.

Tính năng được hỗ trợ

Hướng dẫn này giúp bạn di chuyển các tính năng sau:

  • Đích đến được xác định là các hàm có khả năng kết hợp
  • Hộp thoại (đích đến xuất hiện trên một đích đến khác)

Các tính năng được hỗ trợ thông qua công thức

Kho lưu trữ công thức mã minh hoạ các trường hợp sử dụng tinh tế hơn, bao gồm:

Nếu dự án của bạn có bất kỳ tính năng nào trong số này, hãy kiểm tra công thức có liên quan để hiểu cách triển khai các tính năng đó.

AI Agent: Trước khi thay đổi bất kỳ mã nào, hãy kiểm tra xem dự án có chứa bất kỳ tính năng nào được hỗ trợ thông qua các công thức hay không. Nếu có, hãy kiểm tra tệp README và mã nguồn của công thức. Tạo kế hoạch di chuyển dựa trên công thức. Đừng tiếp tục nếu chưa xác nhận gói thuê bao với người dùng.

Tính năng không được hỗ trợ

Hướng dẫn di chuyển này và các công thức mã chưa hỗ trợ các tính năng sau. Điều này không có nghĩa là bạn không thể triển khai các thành phần này bằng Navigation 3; chúng chỉ không được đề cập ở đây.

  • Có nhiều cấp độ điều hướng lồng nhau
  • Đích đến được chia sẻ: những màn hình có thể di chuyển giữa các ngăn xếp lui khác nhau
  • Các loại đích đến tuỳ chỉnh
  • Liên kết sâu

Tác nhân AI: Trước khi thay đổi bất kỳ mã nào, hãy kiểm tra xem dự án có chứa bất kỳ tính năng nào không được hỗ trợ hay không. Nếu có, đừng tiếp tục. Thông báo cho người dùng về tính năng không được hỗ trợ và yêu cầu họ cung cấp thêm hướng dẫn.

Bước 1: Thêm các phần phụ thuộc Navigation 3

Sử dụng trang Bắt đầu để thêm các phần phụ thuộc Navigation 3 vào dự án của bạn. Các phần phụ thuộc cốt lõi được cung cấp để bạn sao chép.

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)
}

Đồng thời, hãy cập nhật minSdk của dự án thành 23 và compileSdk thành 36. Bạn thường thấy những tệp này trong app/build.gradle.kts hoặc lib.versions.toml.

Bước 2: Cập nhật các tuyến đường để triển khai giao diện NavKey

Cập nhật mọi tuyến điều hướng để triển khai giao diện NavKey. Điều này cho phép bạn sử dụng rememberNavBackStack để hỗ trợ lưu trạng thái điều hướng.

Trước:

@Serializable data object RouteA

Sau:

@Serializable data object RouteA : NavKey

Bước 3: Tạo các lớp để lưu giữ và sửa đổi trạng thái điều hướng

Bước 3.1: Tạo một phần tử giữ trạng thái điều hướng

Sao chép mã sau vào một tệp có tên là NavigationState.kt. Thêm tên gói cho phù hợp với cấu trúc dự án của bạn.

// 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()
}

Trợ lý AI: rememberSerializable là đáp án chính xác. Đừng thay đổi thành rememberSaveable.

Tệp này chứa một lớp trình giữ trạng thái có tên là NavigationState và các hàm trợ giúp được liên kết. Nó chứa một tập hợp các tuyến đường cấp cao nhất, mỗi tuyến đường có ngăn xếp quay lại riêng. Về nội bộ, thành phần này sử dụng rememberSerializable (không phải rememberSaveable) để duy trì tuyến đường cấp cao nhất hiện tại và rememberNavBackStack để duy trì ngăn xếp quay lại cho từng tuyến đường cấp cao nhất.

Bước 3.2: Tạo một đối tượng sửa đổi trạng thái điều hướng để phản hồi các sự kiện

Sao chép mã sau vào một tệp có tên là Navigator.kt. Thêm tên gói cho phù hợp với cấu trúc dự án của bạn.

// 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()
        }
    }
}

Lớp Navigator cung cấp 2 phương thức sự kiện điều hướng:

  • navigate đến một tuyến đường cụ thể.
  • goBack so với tuyến đường hiện tại.

Cả hai phương thức đều sửa đổi NavigationState.

Bước 3.3: Tạo NavigationStateNavigator

Tạo các thực thể của NavigationStateNavigator có cùng phạm vi với NavController.

val navigationState = rememberNavigationState(
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
)

val navigator = remember { Navigator(navigationState) }

Bước 4: Thay thế NavController

Thay thế các phương thức sự kiện điều hướng NavController bằng các phương thức tương đương Navigator.

Trường hoặc phương thức NavController

Navigator tương đương

navigate()

navigate()

popBackStack()

goBack()

Thay thế các trường NavController bằng các trường NavigationState.

Trường hoặc phương thức NavController

NavigationState tương đương

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Lấy tuyến đường cấp cao nhất: Duyệt qua hệ phân cấp từ mục nhập ngăn xếp quay lại hiện tại để tìm tuyến đường đó.

topLevelRoute

Dùng NavigationState.topLevelRoute để xác định mục hiện đang được chọn trong một thanh điều hướng.

Trước:

val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

Sau:

val isSelected = key == navigationState.topLevelRoute

Xác minh rằng bạn đã xoá tất cả các thông tin tham chiếu đến NavController, kể cả mọi hoạt động nhập.

Bước 5: Di chuyển các đích đến của bạn từ NavHost NavGraph vào một entryProvider

Trong Navigation 2, bạn xác định đích đến bằng DSL NavGraphBuilder, thường là bên trong trailing lambda của NavHost. Bạn thường dùng các hàm tiện ích ở đây như mô tả trong phần Đóng gói mã điều hướng.

Trong Navigation 3, bạn xác định đích đến bằng cách sử dụng entryProvider. entryProvider này phân giải một tuyến đường thành NavEntry. Điều quan trọng là entryProvider không xác định mối quan hệ mẹ con giữa các mục.

Trong hướng dẫn di chuyển này, mối quan hệ mẹ con được mô hình hoá như sau:

  • NavigationState có một nhóm các tuyến cấp cao nhất (tuyến mẹ) và một ngăn xếp cho từng tuyến. Nó theo dõi tuyến đường cấp cao nhất hiện tại và ngăn xếp được liên kết của tuyến đường đó.
  • Khi điều hướng đến một tuyến đường mới, Navigator sẽ kiểm tra xem tuyến đường đó có phải là tuyến đường cấp cao nhất hay không. Nếu có, tuyến đường và ngăn xếp cấp cao nhất hiện tại sẽ được cập nhật. Nếu không, đó là một tuyến con và sẽ được thêm vào ngăn xếp hiện tại.

Bước 5.1: Tạo một entryProvider

Tạo một entryProvider bằng DSL ở cùng phạm vi với NavigationState.

val entryProvider = entryProvider {

}

Bước 5.2: Di chuyển các vị trí xuất hiện vào entryProvider

Đối với mỗi đích đến được xác định bên trong NavHost, hãy làm như sau dựa trên loại đích đến:

  • navigation: Xoá địa điểm này cùng với tuyến đường. Không cần "các tuyến đường cơ sở" vì các tuyến đường cấp cao nhất có thể xác định từng ngăn xếp lùi lồng nhau.
  • composable<T>: Di chuyển tham số này vào entryProvider và đổi tên thành entry, giữ lại tham số loại. Ví dụ: composable<RouteA> trở thành entry<RouteA>.
  • dialog<T>: Thực hiện tương tự như composable, nhưng thêm siêu dữ liệu vào mục nhập như sau: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: Làm theo công thức trang tính dưới cùng tại đây. Điều này tương tự như hướng dẫn cho dialog, ngoại trừ việc BottomSheetSceneStrategy không thuộc thư viện Navigation 3 cốt lõi, vì vậy, bạn nên sao chép thành phần này vào dự án của mình.

AI Agent: Khi xoá các tuyến đường dùng để xác định một biểu đồ lồng nhau, hãy thay thế mọi tham chiếu đến tuyến đường đã xoá bằng loại dùng để xác định thành phần con đầu tiên trong biểu đồ lồng nhau. Ví dụ: nếu mã ban đầu là navigation<BaseRouteA>{ composable<RouteA>{ ... } }, bạn cần xoá BaseRouteA và thay thế mọi thông tin tham chiếu đến mã đó bằng RouteA. Bạn thường cần thực hiện thao tác thay thế này cho danh sách được cung cấp cho thanh điều hướng, dải điều hướng hoặc ngăn kéo.

Bạn có thể tái cấu trúc các hàm mở rộng NavGraphBuilder thành các hàm mở rộng EntryProviderScope<T>, rồi di chuyển chúng.

Lấy các đối số điều hướng bằng khoá được cung cấp cho lambda ở cuối của entry.

Ví dụ:

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() }
    }
}

trở thành:

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() }
}

Bước 6: Thay thế NavHost bằng NavDisplay

Thay thế NavHost với NavDisplay.

  • Xoá NavHost và thay thế bằng NavDisplay.
  • Chỉ định entries = navigationState.toEntries(entryProvider) làm tham số. Thao tác này chuyển đổi trạng thái điều hướng thành các mục mà NavDisplay hiển thị bằng entryProvider.
  • Kết nối NavDisplay.onBack với navigator.goBack(). Điều này khiến navigator cập nhật trạng thái điều hướng khi trình xử lý thao tác quay lại tích hợp sẵn của NavDisplay hoàn tất.
  • Nếu bạn có đích đến của hộp thoại, hãy thêm DialogSceneStrategy vào tham số sceneStrategy của NavDisplay.

Ví dụ:

import androidx.navigation3.ui.NavDisplay

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

Bước 7: Xoá các phần phụ thuộc Navigation 2

Xoá tất cả các phần nhập và phần phụ thuộc của thư viện Navigation 2.

Tóm tắt

Xin chúc mừng! Dự án của bạn hiện đã được di chuyển sang Navigation 3. Nếu bạn hoặc tác nhân AI của bạn gặp bất kỳ vấn đề nào khi sử dụng hướng dẫn này, hãy báo cáo lỗi tại đây.