Navigasi dengan Compose

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

Untuk library navigasi alfa terbaru yang dibuat khusus untuk Compose, lihat dokumentasi Navigation 3.

Penyiapan

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

Groovy

dependencies {
    def nav_version = "2.9.5"

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

Kotlin

dependencies {
    val nav_version = "2.9.5"

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

Memulai

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

Untuk mengetahui informasi tentang cara membuat NavController di Compose, lihat bagian Compose di Membuat pengontrol navigasi.

Membuat NavHost

Untuk mengetahui informasi tentang cara membuat NavHost di Compose, lihat bagian Compose di Mendesain grafik navigasi.

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

Untuk informasi tentang cara meneruskan argumen antar-tujuan composable, lihat bagian Compose di Mendesain grafik navigasi.

Mengambil data 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(id = "user1234"))

Objek yang 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 mengakses lapisan data, gunakan SavedStateHandle ViewModel:

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

    private val profile = savedStateHandle.toRoute<Profile>()

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

// …

}

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.

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

@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"

composable<Profile>(
  deepLinks = listOf(
    navDeepLink<Profile>(basePath = "$uri/profile")
  )
) { backStackEntry ->
  ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}

Deep link ini memungkinkan Anda mengatribusikan URL, tindakan, atau jenis mime tertentu dengan composable. 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 dalam contoh sebelumnya, 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 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 komponen:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/profile/$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 mengetahui informasi tentang cara membuat grafik navigasi bertingkat, lihat Grafik bertingkat.

Membangun kolom samping navigasi dan menu navigasi bawah adaptif

NavigationSuiteScaffold menampilkan UI navigasi yang sesuai berdasarkan WindowSizeClass tempat aplikasi Anda dirender. Di layar ringkas, NavigationSuiteScaffold menampilkan menu navigasi bawah; di layar yang diperluas, kolom samping navigasi ditampilkan.

Lihat Membangun navigasi adaptif untuk mengetahui informasi selengkapnya.

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. Fragment kemudian akan menyimpan layar berbasis View, layar Compose, dan layar yang menggunakan View dan Compose. Setelah setiap konten Fragment berada di Compose, langkah berikutnya adalah mengikat semua layar tersebut 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 memungkinkan pengujian setiap composable secara terpisah dari composable NavHost.

Ini berarti Anda tidak boleh meneruskan navController langsung ke composable mana pun, tetapi 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 tidak langsung 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
  • Teruskan lambda yang seharusnya dipicu oleh composable untuk menavigasi, bukan NavController itu sendiri.

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

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

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

@Serializable data class Profile(id: String)

composable<Profile> { backStackEntry ->
    val profile = backStackEntry.toRoute<Profile>()
    ProfileScreen(userId = profile.id) { friendUserId ->
        navController.navigate(route = Profile(id = 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 dependensi pengujian navigasi berikut:

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

Gabungkan NavHost aplikasi Anda dalam composable yang menerima NavHostController sebagai parameter.

@Composable
fun AppNavHost(navController: NavHostController){
  NavHost(navController = navController){ ... }
}

Sekarang Anda dapat menguji AppNavHost dan semua logika navigasi yang ditentukan di dalam NavHost dengan meneruskan instance artefak pengujian navigasi 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.

Sebaiknya klik UI karena Anda ingin menguji penerapan aplikasi yang konkret. 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 saat ini dengan rute yang diharapkan, menggunakan currentBackStackEntry navController:

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

    assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}

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

Pelajari 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 yang lebih canggih di aplikasi modular, termasuk konsep seperti grafik bertingkat dan integrasi menu navigasi bawah, lihat aplikasi Now in Android di GitHub.

Contoh