Navigasi dengan Compose

Komponen Navigasi memberikan dukungan untuk Jetpack Aplikasi Compose. Anda dapat menavigasi antar-composable sambil memanfaatkan infrastruktur komponen Navigasi dan baru.

Penyiapan

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

Groovy

dependencies {
    def nav_version = "2.8.0"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.8.0"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

Memulai

Saat menerapkan navigasi di aplikasi, terapkan host navigasi, grafik, dan pengontrol. Untuk mengetahui informasi selengkapnya, lihat ringkasan Navigasi.

Untuk informasi tentang cara membuat NavController di Compose, lihat kolom Compose dari Membuat pengontrol navigasi.

Membuat NavHost

Untuk informasi tentang cara membuat NavHost di Compose, lihat bagian Tulis Design your navigation graph.

Untuk mengetahui informasi tentang cara menavigasi ke Composable, lihat Menavigasi ke tujuan di arsitektur dokumentasi tambahan.

Navigation Compose juga mendukung penerusan argumen di antara tujuan composable. 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. Parameter arguments dari composable() menerima daftar objek NamedNavArgument. Anda dapat buat NamedNavArgument dengan cepat menggunakan metode navArgument() dan lalu tentukan type persisnya:

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

Anda harus mengekstrak argumen 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 menambahkannya ke rute saat melakukan panggilan navigate:

navController.navigate("profile/user1234")

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

Mengambil data yang kompleks saat menavigasi

Sangat disarankan untuk tidak meneruskan objek data yang kompleks saat menavigasi, tetapi sebagai gantinya teruskan informasi minimum yang diperlukan, seperti ID unik atau bentuk ID lainnya, sebagai argumen saat melakukan tindakan navigasi:

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

Objek kompleks harus disimpan sebagai data dalam satu sumber tepercaya, seperti lapisan data. Begitu Anda tiba di tujuan setelah menavigasi, Anda dapat memuat informasi yang diperlukan dari satu sumber tepercaya dengan menggunakan ID yang diteruskan. Untuk mengambil argumen di ViewModel yang bertanggung jawab untuk yang mengakses lapisan data, gunakan SavedStateHandle dari ViewModel:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

Pendekatan ini membantu mencegah kehilangan data selama perubahan konfigurasi dan inkonsistensi saat objek yang dimaksud sedang diperbarui atau berubah.

Untuk penjelasan yang lebih mendalam tentang alasan Anda harus menghindari penerusan data kompleks sebagai argumen, serta daftar jenis argumen yang didukung, lihat Meneruskan data antar-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 nullable = 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 = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

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

Struktur penanganan argumen melalui rute berarti bahwa composable Anda tetap sepenuhnya independen dari Navigasi dan membuatnya jauh lebih mudah diuji.

Navigation Compose mendukung deep link implisit yang juga dapat ditentukan sebagai bagian dari fungsi composable(). Parameter deepLinks menerima daftar Objek NavDeepLink yang dapat dibuat dengan cepat menggunakan Metode navDeepLink():

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

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

Deep link ini memungkinkan Anda mengaitkan URL, tindakan, atau jenis mime tertentu dengan composable. Secara default, deep link ini tidak diekspos ke aplikasi eksternal. Kepada menyediakan deep link ini secara eksternal, Anda harus menambahkan <intent-filter> ke file manifest.xml aplikasi Anda. Untuk mengaktifkan analisis mendalam dalam contoh sebelumnya, Anda harus menambahkan string berikut ke dalam Elemen <activity> dari manifes:

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

Navigasi otomatis membuat deep link ke dalam composable 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 composable:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.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

Untuk informasi tentang cara membuat grafik navigasi bertingkat, lihat Grafik bertingkat.

Integrasi dengan menu navigasi bawah

Dengan menentukan NavController pada level yang lebih tinggi dalam hierarki composable, Anda dapat menghubungkan Navigasi dengan komponen lain seperti komponen navigasi bawah. Melakukan hal ini memungkinkan Anda untuk menavigasi dengan memilih ikon di bagian bawah .

Untuk menggunakan komponen BottomNavigation dan BottomNavigationItem, tambahkan dependensi androidx.compose.material ke aplikasi Android Anda.

Groovy

dependencies {
    implementation "androidx.compose.material:material:1.7.1"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.7.1")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

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 composable BottomNavigation, dapatkan NavBackStackEntry saat ini menggunakan fungsi currentBackStackEntryAsState(). Entri ini memberi Anda akses ke NavDestination saat ini. Status yang dipilih untuk setiap status BottomNavigationItem kemudian dapat ditentukan dengan membandingkan rute item dengan rute tujuan saat ini dan tujuan induknya ke menangani kasus saat Anda menggunakan navigasi bertingkat menggunakan Hierarki NavDestination.

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 mengangkat status navController dari fungsi NavHost, dan membagikannya ke komponen BottomNavigation. Ini berarti BottomNavigation otomatis memiliki status terbaru.

Keamanan jenis di Navigation Compose

Kode pada halaman ini tidak aman dari error jenis. Anda dapat memanggil navigate() fungsi dengan rute yang tidak ada atau argumen yang salah. Namun, Anda dapat menyusun kode navigasi agar aman dari jenis saat runtime. Dengan begitu, Anda dapat menghindari error dan memastikan bahwa:

  • Argumen yang Anda berikan saat membuka grafik navigasi atau tujuan merupakan jenis yang tepat dan semua argumen yang diperlukan ada.
  • Argumen yang Anda ambil dari SavedStateHandle adalah jenis argumen yang tepat.

Untuk informasi selengkapnya tentang hal ini, lihat Keamanan jenis di DSL dan Navigasi Kotlin Tulis.

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 composable.

Oleh karena itu, rekomendasi untuk aplikasi Compose dan View campuran adalah menggunakan komponen Navigasi berbasis Fragment. Fragmen akan menyimpan berbasis View layar, layar Compose, dan layar yang menggunakan View dan Compose. Setiap kali Konten fragmen ada di Compose, langkah berikutnya adalah mengikat semua layar tersebut bersama dengan Navigation Compose dan menghapus semua Fragment.

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) -> Unit) {
    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

Pisahkan kode navigasi dari tujuan composable untuk mengaktifkan pengujian setiap composable secara terpisah, terpisah dari composable NavHost.

Ini berarti Anda tidak boleh meneruskan navController secara langsung ke composable dan sebagai gantinya meneruskan callback navigasi sebagai parameter. Hal ini memungkinkan semua composable Anda dapat diuji satu per satu, karena tidak memerlukan instance navController dalam pengujian.

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

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

Misalnya, composable Profile yang menggunakan userId sebagai input dan memungkinkan untuk menavigasi ke laman profil teman mungkin memiliki tanda tangan:

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

Dengan cara ini, composable 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 composable Anda:

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

Sebaiknya tulis pengujian yang mencakup persyaratan navigasi aplikasi Anda dengan menguji NavHost, tindakan navigasi yang diteruskan ke composable serta setiap composable layar.

Menguji NavHost

Untuk mulai menguji NavHost , tambahkan pengujian navigasi berikut dependensi:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

Anda dapat menyiapkan subjek pengujian NavHost dan meneruskan sebuah instance dari instance navController ke subjek pengujian tersebut. Untuk melakukannya, navigasi artefak pengujian menyediakan TestNavHostController. Pengujian UI yang memverifikasi tujuan awal aplikasi Anda dan NavHost akan terlihat seperti ini:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    // Unit test
    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

Menguji tindakan navigasi

Anda dapat menguji implementasi navigasi dalam beberapa cara dengan melakukan klik pada elemen UI, lalu memverifikasi tujuan yang ditampilkan atau dengan membandingkan rute yang diharapkan dengan rute saat ini.

Karena Anda ingin menguji penerapan aplikasi konkret, sebaiknya klik UI. Untuk mempelajari cara mengujinya beserta setiap fungsi composable individualnya, pastikan Anda melihat codelab Pengujian di Jetpack Compose.

Anda juga dapat menggunakan navController untuk memeriksa pernyataan dengan membandingkan rute String saat ini dengan yang diharapkan, menggunakan currentBackStackEntry navController:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

Untuk panduan selengkapnya tentang dasar-dasar pengujian Compose, lihat Menguji tata letak Compose dan Pengujian di Jetpack Compose codelab. Untuk mempelajari lebih lanjut pengujian lanjutan kode navigasi, kunjungi panduan Navigasi Pengujian.

Mempelajari lebih lanjut

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

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

Untuk mempelajari implementasi navigasi Compose lanjutan di aplikasi modular, termasuk konsep seperti grafik bertingkat dan menu navigasi bawah lihat aplikasi Now in Android di GitHub.

Contoh