Kỹ năng Android
Xem trên GitHubJetpack Navigation 3
android skills add navigation-3Để di chuyển ứng dụng từ Navigation 2 sang Navigation 3, hãy làm theo các bước sau:
- Thêm các phần phụ thuộc của Navigation 3.
- Cập nhật tuyến đường điều hướng để triển khai giao diện
NavKey. - Tạo các lớp để lưu giữ và sửa đổi trạng thái điều hướng.
- Thay thế
NavControllerbằng các lớp này. - Di chuyển đích đến từ
NavGraphcủaNavHostsangentryProvider. - Thay thế
NavHostvớiNavDisplay. - Xoá các phần phụ thuộc của Navigation 2.
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à 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ợ và không được hỗ trợ khi di chuyển.
Điều kiện tiên quyết
- Bạn phải sử dụng
compileSdktừ 36 trở lên. - Bạn nên làm quen với thuật ngữ điều hướng.
- Đích đến là các hàm composable. Navigation 3 được thiết kế riêng cho Compose. Để sử dụng Mảnh và Khung hiển thị trong Compose, hãy xem bài viết Sử dụng Khung hiển thị trong Compose.
- Các tuyến đường được nhập chính xác. Nếu bạn sử dụng tuyến dựa trên chuỗi, trước tiên hãy di chuyển sang tuyến an toàn về kiểu (ví dụ).
- Không bắt buộc (nhưng nên dùng): Phạm vi 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.
Giả định
Hướng dẫn này đưa ra các giả định sau đây về bạn và dự án của bạn:
- Bạn có một hoặc nhiều tuyến cấp cao nhất (thường xuất hiện trong thanh điều hướng dưới cùng) và mỗi tuyến có ngăn xếp lui 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ả điểm đến của ngăn xếp đó sẽ được giữ lại.
- Bạn luôn thoát khỏi ứng dụng thông qua màn hình Home (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 việc di chuyển từ Navigation 2 sang Navigation 3 trong một thay đổi duy nhất, không thể phân chia. 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.
Tác nhân AI: Trước khi thay đổi mã, hãy kiểm tra xem những giả định này về dự án của người dùng có đúng không. Nếu không, hãy dừng quá trình di chuyển và hỏi người dùng xem nên tiếp tục như thế nào.
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à hàm composable
- 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 nấu ăn
Kho lưu trữ công thức lập trình minh hoạ các trường hợp sử dụng chi tiết hơn, bao gồm:
- Bảng dưới cùng (hướng dẫn được cung cấp trong hướng dẫn này)
- Mã điều hướng được mô-đun hoá và các đích đến được chèn
- Sử dụng và truyền đối số đến
ViewModel - Trả về kết quả từ một màn hình
Nếu dự án của bạn có bất kỳ tính năng nào trong số này, hãy xem công thức liên quan để hiểu cách triển khai các tính năng đó.
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 tính năng nào được hỗ trợ thông qua 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 kế hoạch 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 tính năng đó bằng Navigation 3; chỉ là chúng không được đề cập ở đây.
- Nhiều hơn một cấp điều hướng lồng nhau
- Đích đến dùng chung: 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
Tác nhân AI: Trước khi thay đổi mã, hãy kiểm tra xem dự án có chứa 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ướng dẫn thêm.
Bước 1: Thêm phần phụ thuộc Navigation 3
Sử dụng trang Bắt đầu để thêm các phần phụ thuộc của 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.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)
}
Đồ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
tìm thấy các 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 điều hướ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 phần tử giữ trạng thái điều hướng
Sao chép mã sau đây 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.
// 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() }
Tác nhân AI: rememberSerializable là chính xác. Đừng thay đổi thành
rememberSaveable.
Tệp này chứa một lớp phần tử giữ trạng thái có tên là NavigationState và các hàm trợ giúp liên quan. Thành phần này chứa một tập hợp các tuyến cấp cao nhất, mỗi tuyến có ngăn xếp lui riêng. Trong nội bộ, thành phần này sử dụng rememberSerializable (không phải rememberSaveable) để duy trì tuyến cấp cao nhất hiện tại và rememberNavBackStack để duy trì ngăn xếp lui cho mỗi tuyến 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 đây 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.
// 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 hai phương thức sự kiện điều hướng:
navigateđến một tuyến đường cụ thể.goBackso 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 NavigationState và Navigator
Tạo các thực thể của NavigationState và Navigator 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 |
Tương đương |
|---|---|
|
|
|
|
|
|
Thay thế các trường NavController bằng các trường NavigationState.
Trường hoặc phương thức |
Tương đương |
|---|---|
|
|
|
|
|
|
|
|
Lấy tuyến cấp cao nhất: Di chuyển lên hệ phân cấp từ mục nhập ngăn xếp lui hiện tại để tìm tuyến đó. |
|
Sử dụng NavigationState.topLevelRoute để xác định mục hiện đang được chọn trong 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ả tệp đối chiếu đến NavController, bao gồm cả mọi tệp nhập.
Bước 4.1 Di chuyển logic nhận biết vòng đời
Trong Navigation 2, NavBackStackEntry triển khai LifecycleOwner, cho phép bạn nghe các sự kiện trong vòng đời hoặc thu thập luồng theo cách nhận biết vòng đời bằng cách sử dụng navController.currentBackStackEntry.
Trong Điều hướng 3, NavDisplay cung cấp một LifecycleOwner trong phạm vi điểm truy cập thông qua LocalLifecycleOwner.current cho nội dung composable của mỗi đích đến. Xem phần Vòng đời của đích đến để biết thêm thông tin.
Bạn nên thực hiện các thao tác nhận biết vòng đời ngay trong nội dung composable của đích đến bằng cách tham chiếu LocalLifecycleOwner.current.
Ví dụ: nếu bạn thu thập một luồng theo cách nhận biết vòng đời bằng cách sử dụng mục nhập ngăn xếp lui:
Trước:
// In your destination screen or host val lifecycleOwner = navController.currentBackStackEntry!! val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
Sau:
// Inside the destination composable val state by flow.collectAsStateWithLifecycle()
Bước 4.2 Di chuyển kết quả truyền dữ liệu
Trong Navigation 2, các đích đến sẽ trả kết quả về các đích đến trước đó bằng cách sử dụng SavedStateHandle trên NavBackStackEntry. Vì SavedStateHandle được hỗ trợ bởi trạng thái thực thể đã lưu, nên dữ liệu được trả về sẽ tự động tồn tại sau khi bị buộc tắt.
Trước:
// 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) }
Trong Navigation 3, hãy thêm rememberResultEventBusNavEntryDecorator() vào NavDisplay.entryDecorators. Trong tệp ánh xạ entryProvider của đích gửi, hãy truy xuất LocalResultEventBus.current và gọi sendResult(). Trong đích đến nhận, hãy sử dụng ResultEffect để chuyển tiếp sự kiện đến ViewModel hoặc kích hoạt một tác dụng phụ.
Sau:
// 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) }
Để quan sát dựa trên trạng thái, bạn có thể gọi resultBus.conflateAsState(). Để biết thêm thông tin, hãy xem phần Trả về kết quả.
Bước 5: Di chuyển đích đến từ NavGraph của NavHost sang entryProvider
Trong Navigation 2, bạn xác định đích đến bằng NavGraphBuilder DSL, thường là bên trong trailing lambda của NavHost. Bạn nên sử dụng các hàm mở rộng tại đâ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 entryProvider. entryProvider này phân giải một tuyến đường thành NavEntry. Quan trọng là entryProvider không xác định mối quan hệ mẹ con giữa các mục nhập.
Trong hướng dẫn di chuyển này, mối quan hệ mẹ con được mô hình hoá như sau:
NavigationStatecó một tập hợp các tuyến cấp cao nhất (tuyến mẹ) và một ngăn xếp cho mỗi tuyến. Lớp này theo dõi tuyến cấp cao nhất hiện tại và ngăn xếp liên kết của tuyến đó.- Khi chuyển đến một tuyến mới,
Navigatorsẽ kiểm tra xem tuyến đó có phải là tuyến cấp cao nhất hay không. Nếu có, tuyến và ngăn xếp cấp cao nhất hiện tại sẽ được cập nhật. Nếu không, thì đó là tuyến con và được thêm vào ngăn xếp hiện tại.
Bước 5.1: Tạo entryProvider
Tạo entryProvider bằng DSL trong cùng phạm vi với NavigationState.
val entryProvider = entryProvider<NavKey> { }
Bước 5.2: Di chuyển các đích đế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á cùng với tuyến đường. Không cần "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 lui lồng nhau.composable<T>: Di chuyển lớp này vàoentryProvidervà đổi tên thànhentry, giữ lại tham số loại. Ví dụ:composable<RouteA>trở thànhentry<RouteA>.dialog<T>: Làm 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 cho bảng dưới cùng tại đây. Cách này tương tự như hướng dẫn chodialog, ngoại trừ việcBottomSheetSceneStrategykhông thuộc thư viện Navigation 3 chính, vì vậy, bạn nên sao chép thư viện này vào dự án của mình.
Tác nhân AI: Khi xoá các tuyến dùng để xác định biểu đồ lồng nhau, hãy thay thế mọi tệp đối chiếu đến tuyến đã xoá bằng loại dùng để xác định phần tử con đầu tiên trong biểu đồ lồng nhau. Ví dụ: nếu mã gốc là navigation<BaseRouteA>{ composable<RouteA>{ ... } }, bạn cần xoá BaseRouteA và thay thế mọi tệp đối chiếu bằng RouteA. Việc thay thế này thường cần được thực hiện cho danh sách được cung cấp cho thanh điều hướng, dải điều hướng hoặc ngăn điều hướng.
Bạn có thể cải tiến hàm mở rộng NavGraphBuilder thành
hàm mở rộng EntryProviderScope<T>, sau đó di chuyển các hàm đó.
Lấy đối số điều hướng bằng cách sử dụ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 @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() } } }
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á
NavHostvà thay bằngNavDisplay. - Chỉ định
entries = navigationState.toEntries(entryProvider)làm tham số. Thao tác này sẽ chuyển đổi trạng thái điều hướng thành các mục màNavDisplayhiển thị bằngentryProvider. - Kết nối
NavDisplay.onBackvớinavigator.goBack(). Điều này khiếnnavigatorcập nhật trạng thái điều hướng khi trình xử lý quay lại tích hợp sẵn củaNavDisplayhoàn tất. - Nếu bạn có đích đến của hộp thoại, hãy thêm
DialogSceneStrategyvào tham sốsceneStrategiescủaNavDisplay.
Ví dụ:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Bước 7: Di chuyển đường liên kết sâu
Trong Navigation 2, đường liên kết sâu được xác định trực tiếp bên trong biểu đồ điều hướng bằng cách sử dụng tham số deepLinks của đích đến.
Trong Navigation 3, đường liên kết sâu được quản lý độc lập với giao diện người dùng điều hướng. Bạn xác định DeepLinkMatcher và so khớp các yêu cầu đến trong Hoạt động để tạo ngăn xếp lui ban đầu.
Trước:
Trong Navigation 2, bạn có thể đã xác định một đường liên kết sâu như sau:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Sau:
Trong Navigation 3, bạn xác định một UriDeepLinkMatcher cho tuyến:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Sau đó, trong onCreate (và onNewIntent) của Hoạt động, bạn so khớp ý định đến và khởi chạy ngăn xếp lui:
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 } }
Kiểu đối số tuỳ chỉnh
Trong Navigation 2, bạn đã xử lý các loại đối số tuỳ chỉnh hoặc của bên thứ ba (chẳng hạn như LocalDateTime) bằng cách sử dụng các phương thức triển khai NavType tuỳ chỉnh và typeMap.
Trong Navigation 3, hãy xác định một DeepLinkSerializer để giải tuần tự hoá các loại tuỳ chỉnh hoặc của bên thứ ba từ các tham số URI. Hãy xem phần Tuỳ chỉnh quy trình chuyển đổi tuần tự bằng DeepLinkSerializer để biết thông tin chi tiết.
Để biết thêm các trường hợp sử dụng nâng cao, bao gồm cả ngăn xếp lui tổng hợp và trình so khớp tuỳ chỉnh, hãy xem hướng dẫn Hỗ trợ đường liên kết sâu.
Bước 8: Xoá các phần phụ thuộc của Navigation 2
Xoá tất cả các lệnh nhập và phần phụ thuộc thư viện của 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 vấn đề khi sử dụng hướng dẫn này, hãy báo cáo lỗi tại đây.