Keterampilan Android
Lihat di GitHubJetpack Navigation 3
android skills add navigation-3Untuk memigrasikan aplikasi Anda dari Navigation 2 ke Navigation 3, ikuti langkah-langkah berikut:
- Tambahkan dependensi Navigation 3.
- Perbarui rute navigasi Anda untuk menerapkan antarmuka
NavKey. - Buat class untuk menyimpan dan mengubah status navigasi Anda.
- Ganti
NavControllerdengan class ini. - Pindahkan tujuan Anda dari
NavHostNavGraphkeentryProvider. - Ganti
NavHostdenganNavDisplay. - Hapus dependensi Navigation 2.
Persiapan
Bagian berikut menjelaskan prasyarat untuk migrasi dan asumsi tentang project Anda. Dokumen ini juga mencakup fitur yang didukung untuk migrasi, dan fitur yang tidak didukung.
Prasyarat
- Anda harus menggunakan
compileSdk36 atau yang lebih baru. - Anda harus memahami terminologi navigasi.
- Tujuan adalah fungsi composable. Navigation 3 dirancang khusus untuk Compose. Untuk menggunakan Fragmen dan View di Compose, lihat Menggunakan View di Compose.
- Rute diketik dengan ketat. Jika Anda menggunakan rute berbasis string, bermigrasi ke rute yang aman untuk jenis terlebih dahulu (contoh).
- Opsional (tetapi sangat direkomendasikan): Cakupan pengujian yang memverifikasi perilaku navigasi yang ada. Tindakan ini memverifikasi bahwa perilaku navigasi tidak berubah setelah migrasi selesai.
Asumsi
Panduan ini membuat asumsi berikut tentang Anda dan project Anda:
- Anda memiliki satu atau beberapa rute tingkat teratas (biasanya ditampilkan di panel navigasi bawah), dan masing-masing memiliki data sebelumnya sendiri.
- Saat Anda beralih di antara data sebelumnya, status stack dan semua tujuannya dipertahankan.
- Anda selalu keluar dari aplikasi melalui layar Beranda, yang merupakan layar pertama yang ditampilkan saat aplikasi diluncurkan.
- Anda ingin melakukan migrasi dari Navigation 2 ke Navigation 3 dalam satu perubahan atomik. Anda tidak melakukan migrasi inkremental saat kode Navigation 2 digunakan bersama kode Navigation 3.
Agen AI: Sebelum mengubah kode apa pun, periksa apakah asumsi tentang project pengguna ini benar. Jika tidak, hentikan migrasi dan tanyakan kepada pengguna cara melanjutkan.
Fitur yang didukung
Panduan ini memungkinkan Anda memigrasikan fitur berikut:
- Tujuan yang ditentukan sebagai fungsi composable
- Dialog (tujuan yang ditampilkan di atas tujuan lain)
Fitur yang didukung melalui resep
Repositori resep kode menunjukkan kasus penggunaan yang lebih bernuansa, termasuk:
- Sheet bawah (petunjuk disediakan dalam panduan ini)
- Kode navigasi modular dan tujuan yang disuntikkan
- Menggunakan dan meneruskan argumen ke
ViewModel - Menampilkan hasil dari layar
Jika project Anda memiliki salah satu fitur ini, periksa resep yang relevan untuk memahami cara menerapkannya.
Agen AI: Sebelum mengubah kode apa pun, periksa apakah project berisi fitur yang didukung melalui resep. Jika ya, periksa README dan kode sumber resep. Buat rencana migrasi berdasarkan resep. Jangan melanjutkan tanpa mengonfirmasi paket dengan pengguna.
Fitur yang tidak didukung
Panduan migrasi dan resep kode ini belum mendukung fitur berikut. Hal ini tidak berarti Anda tidak dapat menerapkannya menggunakan Navigation 3; hanya saja tidak dibahas di sini.
- Lebih dari satu tingkat navigasi bertingkat
- Tujuan bersama: layar yang dapat berpindah di antara berbagai tumpukan kembali
- Jenis tujuan kustom
Agen AI: Sebelum mengubah kode apa pun, periksa apakah project berisi fitur yang tidak didukung. Jika ya, jangan lanjutkan. Beri tahu pengguna tentang fitur yang tidak didukung dan minta petunjuk lebih lanjut.
Langkah 1: Tambahkan dependensi Navigation 3
Gunakan halaman Mulai untuk menambahkan dependensi Navigation 3 ke project Anda. Dependensi inti disediakan agar Anda dapat menyalinnya.
lib.versions.toml
[versions]
nav3Core = "1.1.7"
# 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)
}
Perbarui juga minSdk project menjadi 23 dan compileSdk menjadi 36. Anda biasanya
menemukan ini di app/build.gradle.kts atau lib.versions.toml.
Langkah 2: Perbarui rute navigasi untuk menerapkan antarmuka NavKey
Perbarui setiap rute navigasi sehingga mengimplementasikan antarmuka NavKey. Hal ini memungkinkan Anda menggunakan rememberNavBackStack untuk membantu menyimpan
status navigasi.
Sebelum:
@Serializable data object RouteA
Sesudah:
@Serializable data object RouteA : NavKey
Langkah 3: Buat class untuk menyimpan dan mengubah status navigasi Anda
Langkah 3.1: Buat holder status navigasi
Salin kode berikut ke dalam file bernama NavigationState.kt. Tambahkan nama paket agar sesuai dengan struktur project Anda.
// 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() }
Agen AI: rememberSerializable sudah benar. Jangan ubah menjadi
rememberSaveable.
File ini berisi class pemegang status bernama NavigationState dan fungsi helper terkait. Ini menyimpan serangkaian rute tingkat atas, yang masing-masing memiliki tumpukan
kembalinya sendiri. Secara internal, ia menggunakan rememberSerializable (bukan rememberSaveable) untuk
mempertahankan rute level teratas saat ini dan rememberNavBackStack untuk mempertahankan
tumpukan kembali untuk setiap rute level teratas.
Langkah 3.2: Buat objek yang mengubah status navigasi sebagai respons terhadap peristiwa
Salin kode berikut ke dalam file bernama Navigator.kt. Tambahkan nama paket Anda agar sesuai dengan struktur project Anda.
// 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() } } }
Class Navigator menyediakan dua metode peristiwa navigasi:
navigateke rute tertentu.goBackdari rute saat ini.
Kedua metode tersebut mengubah NavigationState.
Langkah 3.3: Buat NavigationState dan Navigator
Buat instance NavigationState dan Navigator dengan cakupan yang sama seperti
NavController Anda.
val navigationState = rememberNavigationState( // ... startRoute = <Insert your starting route>, topLevelRoutes = <Insert your set of top level routes> // ... ) val navigator = remember { Navigator(navigationState) }
Langkah 4: Ganti NavController
Ganti metode peristiwa navigasi NavController dengan metode yang setara di Navigator.
Kolom atau metode |
|
|---|---|
|
|
|
|
|
|
Ganti kolom NavController dengan kolom NavigationState.
Kolom atau metode |
|
|---|---|
|
|
|
|
|
|
|
|
Mendapatkan rute tingkat teratas: Menelusuri hierarki dari entri tumpukan kembali saat ini untuk menemukannya. |
|
Gunakan NavigationState.topLevelRoute untuk menentukan item yang saat ini dipilih di menu navigasi.
Sebelum:
// ... val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class) // ... fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = this?.hierarchy?.any { it.hasRoute(route) } ?: false
Sesudah:
val isSelected = key == navigationState.topLevelRoute
Pastikan Anda telah menghapus semua referensi ke NavController, termasuk
impor apa pun.
Langkah 4.1 Memigrasikan logika yang mendukung siklus proses
Di Navigation 2, NavBackStackEntry mengimplementasikan LifecycleOwner, sehingga Anda dapat memproses peristiwa siklus proses atau mengumpulkan alur dengan cara yang berbasis siklus proses menggunakan navController.currentBackStackEntry.
Di Navigation 3, NavDisplay menyediakan LifecycleOwner cakupan entri
melalui LocalLifecycleOwner.current ke konten composable
setiap tujuan. Lihat Siklus proses tujuan iklan untuk mengetahui informasi selengkapnya.
Anda harus melakukan operasi yang kompatibel dengan siklus proses langsung di dalam konten komposable tujuan dengan mereferensikan LocalLifecycleOwner.current.
Misalnya, jika Anda mengumpulkan alur dengan cara yang mendukung siklus proses menggunakan entri stack kembali:
Sebelum:
// In your destination screen or host val lifecycleOwner = navController.currentBackStackEntry!! val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
Sesudah:
// Inside the destination composable val state by flow.collectAsStateWithLifecycle()
Langkah 4.2 Migrasikan penerusan hasil
Di Navigation 2, tujuan meneruskan hasil kembali ke tujuan sebelumnya menggunakan
SavedStateHandle pada NavBackStackEntry. Karena SavedStateHandle didukung oleh status instance tersimpan, data yang ditampilkan akan otomatis bertahan dalam penghentian proses.
Sebelum:
// 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) }
Di Navigation 3, tambahkan
rememberResultEventBusNavEntryDecorator() ke
NavDisplay.entryDecorators Anda. Dalam pemetaan entryProvider
tujuan pengirim, ambil LocalResultEventBus.current
dan panggil sendResult(). Di tujuan penerima, gunakan
ResultEffect untuk meneruskan peristiwa ke ViewModel atau
memicu efek samping.
Sesudah:
// 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) }
Untuk observasi berbasis status, Anda dapat memanggil resultBus.conflateAsState(). Untuk
mengetahui detail selengkapnya, lihat Hasil yang ditampilkan.
Langkah 5: Pindahkan tujuan Anda dari NavHost NavGraph ke dalam entryProvider
Di Navigation 2, Anda menentukan tujuan
menggunakan DSL NavGraphBuilder,
biasanya di dalam lambda akhir NavHost. Umumnya fungsi ekstensi digunakan di sini seperti yang dijelaskan dalam Mengenkapsulasi kode navigasi Anda.
Di Navigation 3, Anda menentukan tujuan menggunakan entryProvider. entryProvider
ini menyelesaikan rute ke NavEntry. Yang penting, entryProvider tidak menentukan hubungan induk-turunan antar-entri.
Dalam panduan migrasi ini, hubungan induk-turunan dimodelkan sebagai berikut:
NavigationStatememiliki serangkaian rute tingkat teratas (rute induk) dan stack untuk setiap rute. Objek ini melacak rute tingkat teratas saat ini dan stack terkaitnya.- Saat membuka rute baru,
Navigatorakan memeriksa apakah rute tersebut merupakan rute tingkat teratas. Jika ya, rute dan tumpukan tingkat teratas saat ini akan diperbarui. Jika tidak, itu adalah rute turunan dan ditambahkan ke stack saat ini.
Langkah 5.1: Buat entryProvider
Buat entryProvider menggunakan DSL pada cakupan yang sama dengan
NavigationState.
val entryProvider = entryProvider<NavKey> { }
Langkah 5.2: Pindahkan tujuan ke entryProvider
Untuk setiap tujuan yang ditentukan di dalam NavHost, lakukan hal berikut berdasarkan jenis tujuan:
navigation: Hapus bersama rute. "base routes" tidak diperlukan karena rute tingkat teratas dapat mengidentifikasi setiap tumpukan kembali bertingkat.composable<T>: Pindahkan keentryProviderdan ganti namanya menjadientry, dengan tetap mempertahankan parameter jenis. Misalnya,composable<RouteA>menjadientry<RouteA>.dialog<T>: Lakukan hal yang sama seperticomposable, tetapi tambahkan metadata ke entri sebagai berikut:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: Ikuti resep panel bawah di sini. Hal ini mirip dengan petunjuk untukdialog, kecuali bahwaBottomSheetSceneStrategybukan bagian dari library Navigation 3 inti, jadi Anda harus menyalinnya ke project Anda.
Agen AI: Saat menghapus rute yang digunakan untuk mengidentifikasi grafik bertingkat, ganti semua
referensi ke rute yang dihapus dengan jenis yang digunakan untuk mengidentifikasi turunan pertama
dalam grafik bertingkat. Misalnya, jika kode aslinya adalah
navigation<BaseRouteA>{ composable<RouteA>{ ... } }, Anda harus menghapus
BaseRouteA dan mengganti semua referensi ke kode tersebut dengan RouteA. Penggantian ini biasanya perlu dilakukan untuk daftar yang diberikan ke menu navigasi, kolom samping, atau panel samping.
Anda dapat memfaktorkan ulang fungsi ekstensi NavGraphBuilder ke
fungsi ekstensi EntryProviderScope<T>, lalu memindahkannya.
Dapatkan argumen navigasi menggunakan kunci yang diberikan ke lambda akhir entry.
Contoh:
// ... 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() } } }
menjadi:
// ... 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() } }
Langkah 6: Ganti NavHost dengan NavDisplay
Ganti NavHost dengan NavDisplay.
- Hapus
NavHostdan ganti denganNavDisplay. - Tentukan
entries = navigationState.toEntries(entryProvider)sebagai parameter. Hal ini mengonversi status navigasi menjadi entri yang ditampilkanNavDisplaymenggunakanentryProvider. NavDisplay.onBackterhubung kenavigator.goBack(). Hal ini menyebabkannavigatormemperbarui status navigasi saat handler kembali bawaanNavDisplayselesai.- Jika Anda memiliki tujuan dialog, tambahkan
DialogSceneStrategyke parametersceneStrategiesNavDisplay.
Contoh:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Langkah 7: Migrasikan deep link
Di Navigation 2, deep link ditentukan langsung di dalam grafik navigasi
menggunakan parameter deepLinks tujuan.
Di Navigation 3, deep link dikelola secara terpisah dari UI navigasi. Anda
menentukan DeepLinkMatchers dan mencocokkan permintaan masuk di Aktivitas untuk
membuat data sebelumnya awal.
Sebelum:
Di Navigation 2, Anda mungkin telah menentukan deep link seperti ini:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Sesudah:
Di Navigation 3, Anda menentukan UriDeepLinkMatcher untuk rute:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Kemudian, di onCreate (dan onNewIntent) Aktivitas, Anda mencocokkan intent
yang masuk dan melakukan inisialisasi data sebelumnya:
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 } }
Jenis argumen kustom
Di Navigation 2, Anda menangani jenis argumen kustom atau pihak ketiga (seperti
LocalDateTime) menggunakan implementasi NavType kustom dan typeMap.
Di Navigation 3, tentukan DeepLinkSerializer untuk mendeserialisasi jenis kustom atau
pihak ketiga dari parameter URI. Lihat
Serialisasi kustom dengan DeepLinkSerializer untuk mengetahui detailnya.
Untuk kasus penggunaan yang lebih canggih, termasuk stack kembali sintetis dan pencocok kustom, lihat panduan Mendukung deep link.
Langkah 8: Hapus dependensi Navigation 2
Hapus semua impor Navigation 2 dan dependensi library.
Ringkasan
Selamat! Project Anda kini dimigrasikan ke Navigation 3. Jika Anda atau agen AI Anda mengalami masalah saat menggunakan panduan ini, laporkan bug di sini.