Bermigrasi dari Navigation 2 ke Navigation 3

Untuk memigrasikan aplikasi Anda dari Navigation 2 ke Navigation 3, ikuti langkah-langkah berikut:

  1. Tambahkan dependensi Navigation 3.
  2. Perbarui rute navigasi Anda untuk menerapkan antarmuka NavKey.
  3. Buat class untuk menyimpan dan mengubah status navigasi Anda.
  4. Ganti NavController dengan class ini.
  5. Pindahkan tujuan Anda dari NavGraph NavHost ke entryProvider.
  6. Mengganti NavHost dengan NavDisplay.
  7. Hapus dependensi Navigation 2.

Menggunakan Agen AI

Anda dapat menggunakan panduan ini dengan agen AI, seperti Mode Agen Gemini di Android Studio. Baris dalam panduan ini yang diawali dengan "Agen AI:", harus dibaca oleh agen AI, tetapi dapat diabaikan oleh pembaca manusia.

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 compileSdk 36 atau yang lebih baru.
  • Anda harus memahami terminologi navigasi.
  • Tujuan adalah fungsi composable. Navigation 3 dirancang secara eksklusif 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 beberapa rute tingkat atas (biasanya ditampilkan di panel navigasi bawah), dan setiap rute memiliki tumpukan kembali sendiri.
  • Saat Anda beralih antar-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 melanjutkannya.

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:

Jika project Anda memiliki salah satu fitur ini, lihat 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 lanjutkan 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
  • Deep link

Agen AI: Sebelum mengubah kode apa pun, periksa apakah project berisi fitur yang tidak didukung. Jika ya, jangan lanjutkan. Memberi tahu pengguna tentang fitur yang tidak didukung dan meminta 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.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)
}

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 penampung 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 teratas, yang masing-masing memiliki tumpukan kembalinya sendiri. Secara internal, rememberSerializable (bukan rememberSaveable) digunakan 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:

  • navigate ke rute tertentu.
  • goBack dari 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 NavController

Navigator setara

navigate()

navigate()

popBackStack()

goBack()

Ganti kolom NavController dengan kolom NavigationState.

Kolom atau metode NavController

NavigationState setara

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Dapatkan rute tingkat teratas: Jelajahi hierarki dari entri stack kembali saat ini untuk menemukannya.

topLevelRoute

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 5: Pindahkan tujuan Anda dari NavHost NavGraph ke 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:

  • NavigationState memiliki 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, Navigator akan 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 {

}

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 ke entryProvider dan ganti namanya menjadi entry, dengan tetap mempertahankan parameter jenis. Misalnya, composable<RouteA> menjadi entry<RouteA>.
  • dialog<T>: Lakukan hal yang sama seperti composable, tetapi tambahkan metadata ke entri sebagai berikut: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: Ikuti resep sheet bawah di sini. Hal ini mirip dengan petunjuk untuk dialog, kecuali bahwa BottomSheetSceneStrategy bukan 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.

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

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

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

Mengganti NavHost dengan NavDisplay.

  • Hapus NavHost dan ganti dengan NavDisplay.
  • Tentukan entries = navigationState.toEntries(entryProvider) sebagai parameter. Fungsi ini mengonversi status navigasi menjadi entri yang ditampilkan NavDisplay menggunakan entryProvider.
  • Hubungkan NavDisplay.onBack ke navigator.goBack(). Hal ini menyebabkan navigator memperbarui status navigasi saat handler kembali bawaan NavDisplay selesai.
  • Jika Anda memiliki tujuan dialog, tambahkan DialogSceneStrategy ke parameter sceneStrategy NavDisplay.

Contoh:

import androidx.navigation3.ui.NavDisplay

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

Langkah 7: 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.