Menavigasi dengan Compose

Komponen Navigasi memberikan dukungan untuk aplikasi Jetpack Compose. Anda dapat bernavigasi antar-komponen sekaligus memanfaatkan infrastruktur dan fitur komponen Navigasi.

Penyiapan

Untuk mendukung Compose, gunakan dependensi berikut dalam file build.gradle modul aplikasi Anda:

Groovy

dependencies {
    implementation "androidx.navigation:navigation-compose:"
}

Kotlin

dependencies {
    implementation("androidx.navigation:navigation-compose:")
}

Memulai

NavController adalah API pusat untuk komponen Navigasi. API ini bersifat stateful serta memantau data sebelumnya pada komponen yang membentuk layar di aplikasi Anda dan status setiap layar.

Anda dapat membuat NavController dengan menggunakan metode rememberNavController() dalam komponen Anda:

val navController = rememberNavController()

Anda harus membuat NavController di tempat dalam hierarki komponen, yakni tempat semua komponen yang perlu mereferensikannya dapat mengaksesnya. Hal ini mengikuti prinsip penarikan status dan memungkinkan Anda menggunakan NavController dan status yang diberikannya melalui currentBackStackEntryAsState() untuk digunakan sebagai sumber kebenaran demi memperbarui komponen di luar layar. Lihat Integrasi dengan menu navigasi bawah untuk contoh fungsi ini.

Membuat NavHost

Setiap NavController harus diatribusikan dengan satu komponen NavHost. NavHost menautkan NavController dengan grafik navigasi yang menentukan beberapa tujuan komponen yang seharusnya dapat Anda jelajahi di antaranya. Saat Anda menavigasi di antara komponen, konten NavHost akan otomatis direkomposisi. Setiap tujuan komponen dalam grafik navigasi diatribusikan dengan rute.

Membuat NavHost memerlukan NavController yang telah dibuat sebelumnya melalui rememberNavController() dan rute tujuan awal grafik Anda. Pembuatan NavHost menggunakan sintaksis lambda dari Navigation Kotlin DSL untuk membuat grafik navigasi Anda. Anda dapat menambahkan ke struktur navigasi dengan menggunakan metode composable(). Metode ini mengharuskan Anda memberikan rute dan komponen yang harus ditautkan ke tujuan:

NavHost(navController = navController, startDestination = "profile") {
    composable("profile") { Profile(/*...*/) }
    composable("friendslist") { FriendsList(/*...*/) }
    /*...*/
}

Untuk menavigasi ke tujuan komponen di grafik navigasi, Anda harus menggunakan metode navigate(). navigate() mengambil parameter String tunggal yang mewakili rute tujuan. Untuk menavigasi dari komponen dalam grafik navigasi, panggil navigate():

@Composable
fun Profile(navController: NavController) {
    /*...*/
    Button(onClick = { navController.navigate("friends") }) {
        Text(text = "Navigate next")
    }
    /*...*/
}

Anda hanya boleh memanggil navigate() sebagai bagian dari callback, bukan dari komponen itu sendiri, untuk mencegah pemanggilan navigate() pada setiap rekomposisi.

Secara default, navigate() menambahkan tujuan baru Anda ke data sebelumnya. Anda dapat mengubah perilaku navigate dengan melampirkan opsi navigasi tambahan ke panggilan navigate() kami:

// Pop everything up to the "home" destination off the back stack before
// navigating to the "friends" destination
navController.navigate(“friends”) {
    popUpTo("home")
}

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friends" destination
navController.navigate("friends") {
    popUpTo("home") { inclusive = true }
}

// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
    launchSingleTop = true
}

Lihat panduan popUpTo untuk kasus penggunaan lainnya.

Navigation Compose juga mendukung penerusan argumen di antara tujuan komponen. Untuk melakukannya, Anda perlu menambahkan placeholder argumen ke rute, mirip dengan cara Anda menambahkan argumen ke deep link saat menggunakan library navigasi dasar:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

Secara default, semua argumen diuraikan sebagai string. Anda dapat menentukan jenis lain dengan menggunakan parameter arguments untuk menetapkan type:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

Anda harus mengekstrak NavArguments dari NavBackStackEntry yang tersedia di lambda fungsi composable().

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Untuk meneruskan argumen ke tujuan, Anda perlu menambahkan nilai ke rute sebagai ganti placeholder dalam panggilan ke navigate:

navController.navigate("profile/user1234")

Untuk daftar jenis yang didukung, lihat Meneruskan data di antara tujuan.

Menambahkan argumen opsional

Navigation Compose juga mendukung argumen navigasi opsional. Argumen opsional memiliki dua perbedaan dengan argumen yang diwajibkan:

  • Argumen ini harus disertakan menggunakan sintaksis parameter kueri ("?argName={argName}")
  • Argumen harus memiliki defaultValue yang ditetapkan, atau nullability = true (yang secara implisit menetapkan nilai default ke null)

Ini berarti bahwa semua argumen opsional harus ditambahkan secara eksplisit ke fungsi composable() sebagai daftar:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Sekarang, meskipun tidak ada argumen yang diteruskan ke tujuan, defaultValue "me" akan digunakan sebagai gantinya.

Struktur penanganan argumen melalui rute berarti bahwa komponen Anda tetap sepenuhnya independen dari Navigasi dan jauh lebih dapat diuji.

Navigation Compose mendukung deep link implisit yang juga dapat ditentukan sebagai bagian dari fungsi composable(). Anda dapat menambahkannya sebagai daftar menggunakan navDeepLink():

val uri = "https://example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

Deep link ini memungkinkan Anda mengatribusikan URL, tindakan, dan/atau jenis mime tertentu dengan komponen. Secara default, deep link ini tidak diekspos ke aplikasi eksternal. Untuk membuat deep link ini tersedia secara eksternal, Anda harus menambahkan elemen <intent-filter> yang sesuai ke file manifest.xml aplikasi. Untuk mengaktifkan deep link di atas, Anda harus menambahkan yang berikut di dalam elemen <activity> dari manifes:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

Navigasi akan otomatis membuat deep link ke dalam komponen tersebut saat deep link dipicu oleh aplikasi lain.

Deep link yang sama ini juga dapat digunakan untuk mem-build PendingIntent dengan deep link yang sesuai dari komponen:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

Anda selanjutnya dapat menggunakan deepLinkPendingIntent ini seperti PendingIntent lainnya untuk membuka aplikasi di tujuan deep link.

Navigasi Bertingkat

Tujuan dapat dikelompokkan ke dalam grafik bertingkat untuk memodulasi alur tertentu di UI aplikasi. Contohnya adalah alur login mandiri.

Grafik bertingkat mengenkapsulasi tujuannya. Seperti pada grafik root, grafik bertingkat harus memiliki tujuan yang diidentifikasi sebagai tujuan awal menurut rutenya. Ini adalah tujuan yang dituju saat Anda menavigasi ke rute yang terkait dengan grafik bertingkat.

Untuk menambahkan grafik bertingkat ke NavHost, Anda dapat menggunakan fungsi ekstensi navigation:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}

Sebaiknya Anda membagi grafik navigasi menjadi beberapa metode seiring bertambahnya ukuran grafik. Ini juga memungkinkan beberapa modul memberi kontribusi grafik navigasinya sendiri.

fun NavGraphBuilder.loginGraph(navController: NavController) {
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
}

Dengan membuat metode menjadi metode ekstensi di NavGraphBuilder, Anda dapat menggunakannya bersama dengan metode ekstensi navigation, composable, dan dialog yang telah di-build:

NavHost(navController, startDestination = "home") {
    ...
    loginGraph(navController)
    ...
}

Integrasi dengan menu navigasi bawah

Dengan menentukan NavController pada level yang lebih tinggi dalam hierarki komponen, Anda dapat menghubungkan Navigasi dengan komponen lain seperti BottomNavBar. Dengan melakukan hal ini, Anda dapat menavigasi dengan memilih ikon di panel bawah.

Untuk menautkan item di menu navigasi bawah ke rute di grafik navigasi, sebaiknya tetapkan class tertutup, seperti Screen yang terlihat di sini, yang berisi rute dan ID resource string untuk tujuan.

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

Lalu, tempatkan item tersebut dalam daftar yang dapat digunakan oleh BottomNavigationItem:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

Pada komponen BottomNavigation, dapatkan NavBackStackEntry saat ini menggunakan fungsi currentBackStackEntryAsState(). Entri ini memberi Anda akses ke NavDestination saat ini. Status yang dipilih dari setiap BottomNavigationItem selanjutnya dapat ditentukan dengan membandingkan rute item dengan rute tujuan saat ini dan tujuan induknya (untuk menangani kasus saat Anda menggunakan navigasi bertingkat) melalui metode helper hierarchy.

Rute item juga digunakan untuk menghubungkan lambda onClick ke panggilan ke navigate agar mengetuk item akan membuka item tersebut. Dengan menggunakan tanda saveState dan restoreState, status dan data sebelumnya dari item tersebut disimpan dan dipulihkan dengan benar saat Anda beralih di antara item navigasi bawah.

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.route) {
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
              }
              // Avoid multiple copies of the same destination when
              // reselecting the same item
              launchSingleTop = true
              // Restore state when reselecting a previously selected item
              restoreState = true
            }
          }
        )
      }
    }
  }
) { innerPadding ->
  NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { FriendsList(navController) }
  }
}

Di sini, Anda memanfaatkan metode NavController.currentBackStackEntryAsState() untuk menarik status navController dari fungsi NavHost, dan membagikannya dengan komponen BottomNavigation. Ini berarti BottomNavigation otomatis memiliki status terbaru.

Interoperabilitas

Jika ingin menggunakan komponen Navigasi dengan Compose, Anda memiliki dua opsi:

  • Menentukan grafik navigasi dengan komponen Navigasi untuk fragmen.
  • Menentukan grafik navigasi dengan NavHost di Compose menggunakan tujuan Compose. Hal ini hanya dapat dilakukan jika semua layar di grafik navigasi berupa komponen.

Oleh karena itu, rekomendasi untuk aplikasi campuran adalah menggunakan komponen Navigasi berbasis fragmen dan menggunakan fragmen untuk menahan layar berbasis tampilan, layar Compose, dan layar yang menggunakan tampilan dan Compose. Setelah setiap fragmen layar di aplikasi Anda adalah wrapper di sekitar komponen, langkah berikutnya adalah mengikat semua layar tersebut dengan Navigation Compose dan menghapus semua fragmen.

Untuk mengubah tujuan di dalam kode Compose, Anda menampilkan peristiwa yang dapat diteruskan ke dan dipicu oleh setiap komponen dalam hierarki:

@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

Dalam fragmen, Anda membuat bridge antara Compose dan komponen Navigasi berbasis fragmen dengan menemukan NavController dan menavigasi ke tujuan:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

Atau, Anda dapat meneruskan NavController ke hierarki Compose. Namun, menampilkan fungsi sederhana jauh lebih dapat digunakan kembali dan dapat diuji.

Pengujian

Kami sangat merekomendasikan agar Anda memisahkan kode Navigasi dari tujuan komponen untuk memungkinkan pengujian setiap komponen secara terpisah dari komponen NavHost.

Level pengalihan yang disediakan oleh lambda composable memungkinkan Anda memisahkan kode Navigasi dari komponen itu sendiri. Hal ini bekerja dalam dua arah:

  • Hanya meneruskan argumen yang diuraikan ke dalam komponen
  • Meneruskan lambda yang seharusnya dipicu oleh komponen untuk menavigasi, bukan NavController itu sendiri.

Misalnya, komponen Profile yang mengambil userId sebagai input dan memungkinkan pengguna menavigasi ke halaman profil teman mungkin memiliki tanda tangan:

@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

Di sini, kita melihat bahwa komponen Profile berfungsi secara independen dari Navigasi, sehingga membuatnya dapat diuji secara terpisah. Lambda composable akan mengenkapsulasi logika minimal yang diperlukan untuk menjembatani kesenjangan antara Navigation API dan komponen Anda:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

Pelajari lebih lanjut

Untuk mempelajari Navigasi Jetpack lebih lanjut, lihat Memulai komponen Navigasi atau baca codelab Navigasi Jetpack Compose.

Untuk mempelajari cara mendesain navigasi aplikasi agar beradaptasi dengan berbagai ukuran layar, orientasi, dan faktor bentuk, lihat Mengimplementasikan navigasi untuk UI adaptif.