Navigasi Jetpack Compose

1. Pengantar

Terakhir Diperbarui: 25-07-2022

Yang akan Anda butuhkan

Navigasi adalah library Jetpack yang memungkinkan navigasi dari satu tujuan dalam aplikasi ke tujuan lain. Library Navigasi juga menyediakan artefak tertentu agar navigasi dapat konsisten dan idiomatis dengan Jetpack Compose. Artefak ini (navigation-compose) adalah titik fokus codelab ini.

Yang akan Anda lakukan

Anda akan menggunakan Studi Material Rally sebagai dasar dalam codelab ini untuk menerapkan komponen Navigasi Jetpack dan mengaktifkan navigasi antar-layar Rally composable.

Yang akan Anda pelajari

  • Dasar-dasar penggunaan Navigasi Jetpack dengan Jetpack Compose
  • Menavigasi antar-composable
  • Mengintegrasikan composable panel tab khusus ke dalam hierarki navigasi
  • Menavigasi dengan argumen
  • Menavigasi menggunakan deep link
  • Menguji navigasi

2. Penyiapan

Untuk mengikuti, clone titik awal (cabang main) untuk codelab.

$ git clone https://github.com/googlecodelabs/android-compose-codelabs.git

Atau, Anda dapat mendownload dua file ZIP:

Setelah berhasil mendownload kode, buka folder project NavigationCodelab di Android Studio. Sekarang Anda siap untuk memulai.

3. Ringkasan aplikasi Rally

Sebagai langkah pertama, Anda harus memahami aplikasi Rally dan codebase-nya. Jalankan aplikasi dan pelajari sedikit.

Rally memiliki tiga layar utama sebagai composable:

  1. OverviewScreen — ringkasan semua transaksi dan notifikasi keuangan
  2. AccountsScreen — analisis tentang akun yang ada
  3. BillsScreen — biaya terjadwal

Screenshot layar overview (ringkasan) yang berisi informasi tentang Alerts, Accounts, dan Bills. Screenshot Layar Accounts, yang berisi informasi tentang beberapa akun. Screenshot Layar Bills, yang berisi informasi tentang beberapa tagihan keluar.

Di bagian paling atas layar, Rally menggunakan composable panel tab khusus (RallyTabRow) untuk menavigasi di antara ketiga layar ini. Mengetuk setiap ikon akan memperluas pilihan saat ini dan membawa Anda ke layar yang sesuai:

336ba66858ae3728.png e26281a555c5820d.png

Saat membuka layar composable ini, Anda juga dapat menganggapnya sebagai tujuan navigasi karena kita ingin membuka keduanya pada titik tertentu. Tujuan tersebut telah ditentukan sebelumnya dalam file RallyDestinations.kt.

Di dalamnya, Anda akan menemukan ketiga tujuan utama yang ditentukan sebagai objek (Overview, Accounts dan Bills) serta SingleAccount yang akan ditambahkan ke aplikasi nanti. Setiap objek diperluas dari antarmuka RallyDestination dan berisi informasi yang diperlukan di setiap tujuan untuk tujuan navigasi:

  1. icon untuk panel atas
  2. String route (yang diperlukan untuk Navigasi Compose sebagai jalur yang mengarah ke tujuan tersebut)
  3. screen yang mewakili seluruh composable untuk tujuan ini

Saat menjalankan aplikasi, Anda akan menyadari bahwa Anda dapat menavigasi di antara tujuan yang saat ini menggunakan panel atas. Namun, aplikasi tersebut sebenarnya tidak menggunakan Navigasi Compose, tetapi mekanisme navigasinya saat ini mengandalkan sejumlah pengalihan composable manual dan memicu rekomposisi untuk menampilkan konten baru. Oleh karena itu, tujuan codelab ini adalah keberhasilan migrasi dan penerapan Navigasi Compose.

4. Bermigrasi ke Navigasi Compose

Migrasi dasar ke Jetpack Compose mengikuti beberapa langkah:

  1. Menambahkan dependensi Navigasi Compose terbaru
  2. Menyiapkan NavController
  3. Menambahkan NavHost dan membuat grafik navigasi
  4. Menyiapkan rute untuk menavigasi di antara tujuan aplikasi yang berbeda
  5. Mengganti mekanisme navigasi saat ini dengan Navigasi Compose

Mari kita bahas langkah-langkah ini satu per satu secara lebih mendetail.

Menambahkan dependensi Navigasi

Buka file build aplikasi, yang terdapat di app/build.gradle. Di bagian dependensi, tambahkan dependensi navigation-compose.

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

Anda dapat menemukan versi terbaru navigation-compose di sini.

Sekarang, sinkronkan project dan Anda siap untuk mulai menggunakan Navigasi dalam Compose.

Menyiapkan NavController

NavController adalah komponen pusat saat menggunakan Navigasi di Compose. Fungsi ini akan melacak entri composable data sebelumnya, memindahkan stack ke depan, memungkinkan manipulasi data sebelumnya, dan menavigasi di antara status tujuan. NavController harus dibuat sebagai langkah pertama dalam menyiapkan Navigation Compose karena merupakan pusat navigasi.

NavController diperoleh dengan memanggil fungsi rememberNavController(). Tindakan ini akan membuat dan mengingat NavController yang mempertahankan perubahan konfigurasi (menggunakan rememberSaveable).

Anda harus selalu membuat dan menempatkan NavController di tingkat atas dalam hierarki composable, biasanya dalam composable App. Kemudian, semua composable yang perlu mereferensikan NavController akan memiliki akses ke composable tersebut. Hal ini sejalan dengan prinsip penarikan status dan memastikan NavController merupakan sumber kebenaran utama untuk menavigasi di antara layar composable dan mempertahankan data sebelumnya.

Buka RallyActivity.kt. Ambil NavController menggunakan rememberNavController() dalam RallyApp karena merupakan composable root dan titik entri untuk seluruh aplikasi:

import androidx.navigation.compose.rememberNavController
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            // ...
        ) {
            // ...
       }
}

Rute di Navigasi Compose

Seperti yang telah disebutkan sebelumnya, Rally App memiliki tiga tujuan utama dan satu tujuan tambahan untuk ditambahkan nanti (SingleAccount). Ini didefinisikan dalam RallyDestinations.kt. dan kita menyebutkan bahwa setiap tujuan memiliki icon, route, dan screen yang telah ditentukan:

Screenshot layar overview (ringkasan) yang berisi informasi tentang Alerts, Accounts, dan Bills. Screenshot Layar Accounts, yang berisi informasi tentang beberapa akun. Screenshot Layar Bills, yang berisi informasi tentang beberapa tagihan keluar.

Langkah berikutnya adalah menambahkan tujuan ini ke grafik navigasi, dengan Overview sebagai tujuan awal saat aplikasi diluncurkan.

Saat menggunakan Navigasi dalam Compose, setiap tujuan composable di grafik navigasi akan dikaitkan dengan rute. Rute direpresentasikan sebagai String yang menentukan jalur ke composable dan memandu navController untuk muncul di tempat yang tepat. Anda bisa menganggapnya sebagai deep link implisit yang mengarah ke tujuan tertentu. Setiap tujuan harus memiliki rute yang unik.

Untuk melakukannya, kita akan menggunakan properti route dari setiap objek RallyDestination. Misalnya, Overview.route adalah rute yang akan mengarahkan Anda ke composable layar Overview.

Memanggil composable NavHost dengan grafik navigasi

Langkah berikutnya adalah menambahkan NavHost dan membuat grafik navigasi.

Tiga bagian utama Navigasi adalah NavController, NavGraph, dan NavHost. NavController selalu dikaitkan dengan satu composable NavHost. NavHost berfungsi sebagai penampung dan bertanggung jawab untuk menampilkan tujuan grafik saat ini. Saat Anda menavigasi antar-composable, konten NavHost akan otomatis direkomposisi. Konten ini juga akan menautkan NavController dengan grafik navigasi ( NavGraph) yang memetakan tujuan composable yang akan dinavigasi. Pada dasarnya, konten ini merupakan kumpulan tujuan yang dapat diambil.

Kembali ke composable RallyApp di RallyActivity.kt. Ganti composable Box di dalam Scaffold yang berisi konten layar saat ini untuk peralihan layar secara manual dengan NavHost baru yang dapat Anda buat dengan mengikuti contoh kode di bawah.

Teruskan navController yang telah kita buat di langkah sebelumnya untuk menghubungkannya ke NavHost ini. Seperti yang disebutkan sebelumnya, setiap NavController harus diatribusikan dengan satu NavHost.

NavHost juga memerlukan rute startDestination untuk mengetahui tujuan yang akan ditampilkan saat aplikasi diluncurkan. Jadi, setel ke Overview.route. Selain itu, teruskan Modifier untuk menerima padding Scaffold luar dan terapkan ke NavHost.

Parameter akhir builder: NavGraphBuilder.() -> Unit bertanggung jawab untuk menentukan dan membuat grafik navigasi. Fungsi ini menggunakan sintaksis lambda dari Navigation Kotlin DSL sehingga dapat diteruskan sebagai lambda akhir di dalam isi fungsi dan dikeluarkan dari tanda kurung:

import androidx.navigation.compose.NavHost
...

Scaffold(...) { innerPadding ->
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = Modifier.padding(innerPadding)
    ) {
       // builder parameter will be defined here as the graph
    }
}

Menambahkan tujuan ke NavGraph

Sekarang, Anda dapat menentukan grafik navigasi dan tujuan yang dapat dinavigasi oleh NavController. Seperti yang telah disebutkan, parameter builder meminta sebuah fungsi. Jadi, Navigasi Compose akan menyediakan fungsi ekstensi NavGraphBuilder.composable agar dapat dengan mudah menambahkan setiap tujuan composable ke grafik navigasi dan menentukan informasi navigasi yang diperlukan.

Tujuan pertama adalah Overview sehingga Anda perlu menambahkannya melalui fungsi ekstensi composable dan menetapkan String route yang unik. Tindakan ini hanya akan menambahkan tujuan ke grafik navigasi sehingga Anda juga perlu menentukan UI yang sebenarnya yang akan ditampilkan saat menavigasi ke tujuan ini. Tindakan ini juga akan dilakukan melalui lambda akhir di dalam isi fungsi composable, pola yang sering digunakan di Compose:

import androidx.navigation.compose.composable
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
}

Dengan mengikuti pola ini, kita akan menambahkan ketiga composable layar utama sebagai tiga tujuan:

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
    composable(route = Accounts.route) {
        Accounts.screen()
    }
    composable(route = Bills.route) {
        Bills.screen()
    }
}

Sekarang jalankan aplikasi - Anda akan melihat Overview sebagai tujuan awal dan UI-nya yang sesuai.

Kita sudah menyebutkan sebelum composable tab atas kustom, composable RallyTabRow, yang sebelumnya menangani navigasi manual antar-layar. Saat ini, pengujian belum terhubung dengan navigasi baru sehingga Anda dapat memverifikasi bahwa mengklik tab tidak akan mengubah tujuan composable layar yang ditampilkan. Mari kita perbaiki nanti.

5. Mengintegrasikan RallyTabRow dengan navigasi

Pada langkah ini, Anda akan menghubungkan RallyTabRow dengan navController dan grafik navigasi agar dapat menavigasikannya ke tujuan yang benar.

Untuk melakukannya, Anda harus menggunakan navController baru guna menentukan tindakan navigasi yang benar untuk callback onTabSelected RallyTabRow. Callback ini menentukan peristiwa yang akan terjadi jika ikon tab tertentu dipilih dan melakukan tindakan navigasi melalui navController.navigate(route)..

Dengan mengikuti panduan ini, di RallyActivity, temukan composable RallyTabRow dan parameter callback onTabSelected.

Anda juga perlu mengetahui ikon tab tepat yang dipilih karena kita ingin tab menavigasi ke tujuan tertentu saat diketuk. Untungnya, parameter onTabSelected: (RallyDestination) -> Unit sudah menyediakan ini. Anda akan menggunakan informasi tersebut dan rute RallyDestination untuk memandu navController dan memanggil navController.navigate(newScreen.route) saat tab dipilih:

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    // Pass the callback like this,
                    // defining the navigation action when a tab is selected:
                    onTabSelected = { newScreen ->
                        navController.navigate(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }

Jika menjalankan aplikasi sekarang, Anda dapat memverifikasi bahwa mengetuk setiap tab di RallyTabRow memang akan membuka tujuan composable yang benar. Namun, saat ini ada dua masalah yang mungkin Anda temukan:

  1. Mengetuk tab yang sama dalam satu baris akan meluncurkan beberapa salinan dari tujuan yang sama
  2. UI tab tidak sesuai dengan tujuan yang ditampilkan saat ini. Ini artinya perluasan dan penyingkatan tab yang dipilih tidak berfungsi sebagaimana mestinya:

336ba66858ae3728.png e26281a555c5820d.png

Mari kita perbaiki keduanya.

Meluncurkan satu salinan tujuan

Untuk memperbaiki masalah pertama dan memastikan akan ada maksimal satu salinan tujuan tertentu di bagian atas data sebelumnya, Compose Navigation API menyediakan tanda launchSingleTop yang dapat Anda teruskan ke navController.navigate(), seperti ini:

navController.navigate(route) { launchSingleTop = true }

Anda menginginkan perilaku ini ada di seluruh aplikasi untuk setiap tujuan, bukan menyalin dan menempelkan tanda ini ke semua panggilan .navigate(...), Anda dapat langsung mengekstraknya ke dalam ekstensi helper di bagian bawah RallyActivity:

import androidx.navigation.NavHostController
// ...

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { launchSingleTop = true }

Sekarang Anda dapat mengganti panggilan navController.navigate(newScreen.route) dengan .navigateSingleTopTo(...). Jalankan kembali aplikasi dan verifikasi bahwa Anda sekarang hanya akan mendapatkan satu salinan dari satu tujuan ketika mengklik ikonnya beberapa kali di panel atas:

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    onTabSelected = { newScreen ->
                        navController
                            .navigateSingleTopTo(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }

Mengontrol opsi navigasi dan status data sebelumnya

Selain launchSingleTop, ada juga tanda lain yang dapat Anda gunakan dari NavOptionsBuilder untuk mengontrol dan menyesuaikan perilaku navigasi lebih lanjut. RallyTabRow berfungsi mirip dengan BottomNavigation. Oleh karena itu, Anda juga harus memikirkan apakah ingin menyimpan dan memulihkan status tujuan saat Anda menavigasi ke dan dari status tersebut. Misalnya, jika Anda men-scroll ke bagian bawah Ringkasan, lalu membuka Akun dan kembali, apakah Anda ingin mempertahankan posisi scroll? Ingin mengetuk ulang di tujuan yang sama di RallyTabRow untuk memuat ulang status layar Anda atau tidak? Semua ini adalah pertanyaan yang valid dan harus ditentukan oleh persyaratan desain aplikasi Anda sendiri.

Kita akan membahas beberapa opsi tambahan yang dapat digunakan dalam fungsi ekstensi navigateSingleTopTo yang sama:

  • launchSingleTop = true - seperti yang telah disebutkan, ini memastikan akan ada maksimal satu salinan tujuan tertentu di bagian atas data sebelumnya
  • Dalam aplikasi Rally, mengetuk ulang tab yang sama beberapa kali tidak akan meluncurkan beberapa salinan tujuan yang sama
  • popUpTo(startDestination) { saveState = true } - muncul ke tujuan awal grafik untuk menghindari penumpukan tujuan yang besar pada data sebelumnya saat Anda memilih tab
  • Dalam Rally, menekan panah kembali dari tujuan mana pun akan memunculkan seluruh data sebelumnya ke Ringkasan
  • restoreState = true - menentukan apakah tindakan navigasi ini harus memulihkan status yang sebelumnya disimpan oleh atribut PopUpToBuilder.saveState atau popUpToSaveState. Perhatikan bahwa, jika tidak ada status yang sebelumnya telah disimpan dengan ID tujuan yang dituju, hal ini tidak akan berpengaruh
  • Dalam Rally, mengetuk kembali tab yang sama akan mempertahankan data dan status pengguna sebelumnya di layar tanpa memuat ulang lagi

Anda dapat menambahkan semua opsi ini satu per satu ke kode, menjalankan aplikasi setelah melakukan verifikasi, dan memverifikasi perilaku yang tepat setelah menambahkan setiap flag. Dengan demikian, Anda dapat melihat langsung bagaimana setiap flag mengubah navigasi dan status data sebelumnya:

import androidx.navigation.NavHostController
import androidx.navigation.NavGraph.Companion.findStartDestination
// ...

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) {
        popUpTo(
            this@navigateSingleTopTo.graph.findStartDestination().id
        ) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
}

Memperbaiki UI tab

Di awal codelab, saat masih menggunakan mekanisme navigasi manual, RallyTabRow menggunakan variabel currentScreen untuk menentukan apakah akan meluaskan atau menciutkan setiap tab.

Namun, setelah perubahan yang Anda buat, currentScreen tidak akan diperbarui lagi. Inilah sebabnya memperluas dan menciutkan tab yang dipilih di dalam RallyTabRow tidak berfungsi lagi.

Untuk mengaktifkan kembali perilaku ini menggunakan Navigasi Compose, Anda harus mengetahui tujuan saat ini mana yang ditampilkan di setiap titik, atau dalam istilah navigasi, apa yang ada di bagian atas entri data sebelumnya saat ini, lalu memperbarui RallyTabRow setiap kali ini berubah.

Untuk mendapatkan pembaruan real-time pada tujuan Anda saat ini dari data sebelumnya dalam bentuk State, Anda dapat menggunakannavController.currentBackStackEntryAsState() lalu mengambildestination:

import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()

        val currentBackStack by navController.currentBackStackEntryAsState()
        // Fetch your currentDestination:
        val currentDestination = currentBackStack?.destination
        // ...
    }
}

currentBackStack?.destination akan menampilkan NavDestination. Untuk memperbarui currentScreen lagi dengan benar, Anda harus menemukan cara untuk mencocokkan pengembalian NavDestination dengan salah satu dari tiga composable layar utama Rally. Anda harus menentukan composable mana yang saat ini ditampilkan sehingga Anda dapat meneruskan informasi ini ke RallyTabRow. Seperti yang telah disebutkan sebelumnya, setiap tujuan memiliki rute unik sehingga kita dapat menggunakan rute String ini sebagai ID untuk melakukan perbandingan yang terverifikasi dan menemukan kecocokan yang unik.

Untuk memperbarui currentScreen, Anda harus melakukan iterasi melalui daftar rallyTabRowScreens untuk menemukan rute yang cocok, lalu menampilkan RallyDestination yang sesuai. Kotlin menyediakan fungsi .find() yang praktis untuk hal ini:

import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()

        val currentBackStack by navController.currentBackStackEntryAsState()
        val currentDestination = currentBackStack?.destination

        // Change the variable to this and use Overview as a backup screen if this returns null
        val currentScreen = rallyTabRowScreens.find { it.route == currentDestination?.route } ?: Overview
        // ...
    }
}

Anda dapat menjalankan aplikasi dan memastikan UI panel tab kini diperbarui karena currentScreen sudah diteruskan ke RallyTabRow.

6. Mengekstrak composable layar dari RallyDestinations

Agar lebih mudah, hingga saat ini kami menggunakan properti screen dari antarmuka RallyDestination dan objek layar yang diperluas darinya, untuk menambahkan UI composable di NavHost (RallyActivity.kt):

import com.example.compose.rally.ui.overview.OverviewScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
    // ...
}

Namun, langkah-langkah berikut dalam codelab ini (seperti peristiwa klik) memerlukan penerusan informasi tambahan ke layar composable. Dalam lingkungan produksi, pasti akan ada lebih banyak data yang perlu diteruskan.

Cara yang tepat dan lebih mudah untuk mencapai hal ini adalah dengan menambahkan composable secara langsung dalam grafik navigasi NavHost dan mengekstraknya dari RallyDestination. Setelah itu, RallyDestination dan objek layar hanya akan menyimpan informasi khusus navigasi, seperti icon dan route, dan akan dipisahkan dari semua hal yang terkait dengan Compose UI.

Buka RallyDestinations.kt. Ekstrak setiap composable layar dari parameter screen objek RallyDestination dan ke fungsi composable yang sesuai di NavHost untuk menggantikan panggilan .screen() sebelumnya, seperti ini:

import com.example.compose.rally.ui.accounts.AccountsScreen
import com.example.compose.rally.ui.bills.BillsScreen
import com.example.compose.rally.ui.overview.OverviewScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        OverviewScreen()
    }
    composable(route = Accounts.route) {
        AccountsScreen()
    }
    composable(route = Bills.route) {
        BillsScreen()
    }
}

Pada tahap ini, Anda dapat menghapus parameter screen dengan aman dari RallyDestination dan objeknya:

interface RallyDestination {
    val icon: ImageVector
    val route: String
}

/**
 * Rally app navigation destinations
 */
object Overview : RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
}
// ...

Jalankan lagi aplikasi dan pastikan semuanya masih berfungsi seperti sebelumnya. Setelah menyelesaikan langkah ini, Anda dapat menyiapkan peristiwa klik di dalam layar composable.

Mengaktifkan klik di OverviewScreen

Saat ini, semua peristiwa klik di OverviewScreen akan diabaikan. Artinya, tombol "SEE ALL" subbagian Accounts and Bills (Akun dan Tagihan) dapat diklik, tetapi sebenarnya tidak akan membawa Anda ke mana pun. Sasaran langkah ini adalah mengaktifkan navigasi untuk peristiwa klik ini.

Rekaman layar overview (ringkasan), yang men-scroll ke tujuan klik akhir dan mencoba mengklik. Klik tidak berfungsi karena belum diterapkan.

Composable OverviewScreen dapat menerima beberapa fungsi sebagai callback untuk ditetapkan sebagai peristiwa klik yang harus menjadi tindakan navigasi yang membawa Anda ke AccountsScreen atau BillsScreen. Mari teruskan callback navigasi ini ke onClickSeeAllAccounts dan onClickSeeAllBills untuk menavigasi ke tujuan yang relevan.

Buka RallyActivity.kt, temukan OverviewScreen dalam NavHost dan teruskan navController.navigateSingleTopTo(...) ke kedua callback navigasi dengan rute yang sesuai:

OverviewScreen(
    onClickSeeAllAccounts = {
        navController.navigateSingleTopTo(Accounts.route)
    },
    onClickSeeAllBills = {
        navController.navigateSingleTopTo(Bills.route)
    }
)

Sekarang navController akan memiliki informasi yang memadai, seperti rute tujuan yang tepat,untuk bernavigasi ke tujuan yang tepat dengan mengklik tombol. Jika melihat implementasi OverviewScreen, Anda akan melihat bahwa callback ini sudah ditetapkan ke parameter onClick yang sesuai:

@Composable
fun OverviewScreen(...) {
    // ...
    AccountsCard(
        onClickSeeAll = onClickSeeAllAccounts,
        onAccountClick = onAccountClick
    )
    // ...
    BillsCard(
        onClickSeeAll = onClickSeeAllBills
    )
}

Seperti yang sudah disebutkan sebelumnya, mempertahankan navController di tingkat teratas hierarki navigasi dan mengangkatnya ke tingkat composable App (bukan meneruskannya langsung ke, misalnya, OverviewScreen) akan memudahkan pratinjau, penggunaan kembali, dan pengujian composable OverviewScreen secara terpisah, tanpa harus bergantung pada instance navController aktual atau tiruan. Meneruskan callback akan memungkinkan perubahan cepat pada peristiwa klik.

7. Menavigasi ke SingleAccountScreen dengan argumen

Mari tambahkan beberapa fungsi baru ke layar Accounts dan Overview. Saat ini, layar tersebut menampilkan daftar beberapa jenis akun - "Giro", Tabungan untuk Rumah", dll.

2f335ceab09e449a.png 2e78a5e090e3fccb.png

Namun, tidak akan ada yang berubah jika Anda mengklik jenis akun ini (mungkin belum). Mari kita perbaiki. Saat mengetuk setiap jenis akun, kita ingin menampilkan layar baru dengan detail akun lengkap. Untuk melakukannya, kita harus memberikan informasi tambahan ke navController tentang jenis akun yang kita klik. Tindakan ini dapat dilakukan melalui argumen.

Argumen adalah alat yang sangat canggih dan dapat membuat pemilihan rute navigasi dinamis dengan meneruskan satu atau beberapa argumen ke suatu rute. Dengan bantuan ini, Anda dapat menampilkan informasi yang berbeda berdasarkan berbagai argumen yang diberikan.

Di RallyApp, tambahkan SingleAccountScreen tujuan baru, yang akan menangani penayangan akun individual ini, ke grafik dengan menambahkan fungsi composable baru ke NavHost: yang ada

import com.example.compose.rally.ui.accounts.SingleAccountScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    ...
    composable(route = SingleAccount.route) {
        SingleAccountScreen()
    }
}

Menyiapkan tujuan landing SingleAccountScreen

Saat Anda tiba di SingleAccountScreen, tujuan ini akan memerlukan informasi tambahan untuk mengetahui jenis akun persis yang harus ditampilkan saat dibuka. Kita dapat menggunakan argumen untuk meneruskan informasi semacam ini. Anda perlu menentukan bahwa rutenya juga memerlukan {account_type} argumen. Jika melihat RallyDestination dan objek SingleAccount-nya, Anda akan melihat bahwa argumen ini telah ditentukan untuk Anda gunakan, sebagai String accountTypeArg.

Untuk meneruskan argumen di sepanjang rute saat menavigasi, Anda harus menambahkannya bersama-sama, mengikuti pola: "route/{argument}". Dalam kasus Anda, formatnya akan terlihat seperti ini: "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}". Ingat bahwa tanda $ digunakan untuk meng-escape variabel:

import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...

composable(
    route =
        "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
) {
    SingleAccountScreen()
}

Ini akan memastikan bahwa, saat tindakan dipicu untuk menavigasi ke SingleAccountScreen, argumen accountTypeArg juga harus diteruskan, jika tidak, navigasi akan gagal. Anggap ini sebagai tanda tangan atau kontrak yang harus diikuti oleh tujuan lain yang ingin menuju ke SingleAccountScreen.

Langkah kedua adalah untuk membuat composable ini sadar bahwa argumen harus diterima. Anda melakukan hal tersebut dengan menentukan parameter arguments-nya. Anda dapat menentukan argumen sebanyak yang dibutuhkan karena fungsi composable secara default menerima daftar argumen. Dalam kasus ini, Anda hanya perlu menambahkan satu panggilan bernama accountTypeArg dan menambahkan keamanan tambahan dengan menentukannya sebagai jenis String. Jika Anda tidak menetapkan jenis secara eksplisit, jenis tersebut akan disimpulkan dari nilai default argumen ini:

import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...

composable(
    route =
        "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
    arguments = listOf(
        navArgument(SingleAccount.accountTypeArg) { type = NavType.StringType }
    )
) {
    SingleAccountScreen()
}

Pendekatan ini akan berfungsi dengan sempurna dan Anda dapat memilih untuk menyimpan kode seperti ini. Namun, karena semua informasi khusus tujuan kita ada di RallyDestinations.kt dan objeknya, mari terus gunakan pendekatan yang sama (seperti yang sudah kita lakukan di atas untuk Overview, Accounts,, dan Bills) dan pindahkan daftar argumen ini ke dalam SingleAccount:

object SingleAccount : RallyDestination {
    // ...
    override val route = "single_account"
    const val accountTypeArg = "account_type"
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
}

Ganti argumen sebelumnya dengan SingleAccount.arguments yang sekarang kembali ke NavHost yang sesuai dengan composable. Penggantian argumen ini juga akan memastikan bahwa kita menjaga NavHost tetap sederhana dan mudah dibaca:

composable(
    route = "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
    arguments =  SingleAccount.arguments
) {
    SingleAccountScreen()
}

Setelah Anda menentukan rute lengkap dengan argumen untuk SingleAccountScreen, langkah berikutnya adalah memastikan accountTypeArg ini diteruskan lebih lanjut ke composable SingleAccountScreen sehingga dapat mengetahui jenis akun yang akan ditampilkan dengan benar. Jika melihat implementasi SingleAccountScreen, Anda akan mengetahui bahwa implementasi telah disiapkan dan menunggu untuk menerima parameter accountType:

fun SingleAccountScreen(
    accountType: String? = UserData.accounts.first().name
) {
   // ...
}

Sebagai ringkasan, sejauh ini:

  • Anda telah memastikan bahwa kita menentukan rute untuk meminta argumen sebagai sinyal ke tujuan sebelumnya
  • Anda memastikan bahwa composable tahu bahwa aplikasi harus menerima argumen

Langkah terakhir kita adalah mengambil nilai argumen yang diteruskan.

Di Navigasi Compose, setiap fungsi composable NavHost memiliki akses ke NavBackStackEntry saat ini - class yang menyimpan informasi tentang rute saat ini dan meneruskan argumen entri di data sebelumnya. Anda dapat menggunakan ini untuk mendapatkan daftar arguments yang diperlukan dari navBackStackEntry, lalu menelusuri dan mengambil argumen yang tepat yang Anda perlukan, untuk meneruskannya lebih jauh ke layar composable.

Dalam hal ini, Anda akan meminta accountTypeArg dari navBackStackEntry. Kemudian, Anda harus meneruskannya lebih lanjut ke parameter accountType SingleAccountScreen'.

Anda juga dapat memberikan nilai default untuk argumen, sebagai placeholder, jika nilai belum disediakan dan nilai ini dapat membuat kode Anda lebih aman dengan mencakup kasus ekstrem ini.

Sekarang kode akan terlihat seperti ini:

NavHost(...) {
    // ...
    composable(
        route =
          "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
        arguments = SingleAccount.arguments
    ) { navBackStackEntry ->
        // Retrieve the passed argument
        val accountType =
            navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)

        // Pass accountType to SingleAccountScreen
        SingleAccountScreen(accountType)
    }
}

Sekarang SingleAccountScreen memiliki informasi yang diperlukan untuk menampilkan jenis akun yang benar saat Anda membukanya. Jika melihat implementasi SingleAccountScreen,, Anda dapat mengetahui bahwa implementasi tersebut sudah melakukan pencocokan dengan accountType yang diteruskan ke sumber UserData untuk mengambil detail akun yang sesuai.

Mari lakukan satu tugas pengoptimalan kecil lagi dan pindahkan juga rute "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}" ke RallyDestinations.kt dan objek SingleAccount-nya:

object SingleAccount : RallyDestination {
    // ...
    override val route = "single_account"
    const val accountTypeArg = "account_type"
    val routeWithArgs = "${route}/{${accountTypeArg}}"
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
}

Sekali lagi, ganti di NavHost composable: yang sesuai

// ...
composable(
    route = SingleAccount.routeWithArgs,
    arguments = SingleAccount.arguments
) {...}

Menyiapkan tujuan awal Accounts (Akun) dan Overview (Ringkasan)

Setelah menentukan rute SingleAccountScreen dan argumen yang diperlukan serta menerima untuk membuat navigasi yang berhasil ke SingleAccountScreen, Anda harus memastikan bahwa argumen accountTypeArg yang sama diteruskan dari tujuan sebelumnya (artinya, tujuan mana pun tempat Anda berasal).

Seperti yang dapat Anda lihat, tujuan ini memiliki dua sisi, yaitu tujuan awal yang menyediakan dan meneruskan argumen, dan tujuan landing yang menerima argumen tersebut dan menggunakannya untuk menampilkan informasi yang benar. Keduanya harus ditentukan secara eksplisit.

Misalnya, saat Anda berada di tujuan Accounts dan mengetuk jenis akun "Checking" (Giro), tujuan Accounts harus meneruskan String "Checking" sebagai argumen, yang ditambahkan ke rute String "single_account", ke berhasil membuka SingleAccountScreen yang sesuai. Rute String akan terlihat seperti ini: "single_account/Checking"

Anda akan menggunakan rute yang sama persis ini dengan argumen yang diteruskan saat menggunakan navController.navigateSingleTopTo(...), seperti ini:

navController.navigateSingleTopTo("${SingleAccount.route}/$accountType").

Teruskan callback tindakan navigasi ini ke parameter onAccountClick dari OverviewScreen dan AccountsScreen. Perlu diketahui bahwa parameter tersebut telah ditetapkan sebelumnya sebagai: onAccountClick: (String) -> Unit, dengan String sebagai input. Artinya, ketika pengguna mengetuk jenis akun tertentu di Overview dan Account, String jenis akun tersebut sudah akan tersedia untuk Anda dan dapat dengan mudah diteruskan sebagai argumen navigasi:

OverviewScreen(
    // ...
    onAccountClick = { accountType ->
        navController
          .navigateSingleTopTo("${SingleAccount.route}/$accountType")
    }
)
// ...

AccountsScreen(
    // ...
    onAccountClick = { accountType ->
        navController
          .navigateSingleTopTo("${SingleAccount.route}/$accountType")
    }
)

Agar mudah dibaca, Anda dapat mengekstrak tindakan navigasi ini ke fungsi bantuan pribadi dengan ekstensi:

import androidx.navigation.NavHostController
// ...
OverviewScreen(
    // ...
    onAccountClick = { accountType ->
        navController.navigateToSingleAccount(accountType)
    }
)

// ...

AccountsScreen(
    // ...
    onAccountClick = { accountType ->
        navController.navigateToSingleAccount(accountType)
    }
)

// ...

private fun NavHostController.navigateToSingleAccount(accountType: String) {
    this.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}

Saat menjalankan aplikasi pada tahap ini, Anda dapat mengklik setiap jenis akun dan akan diarahkan ke SingleAccountScreen yang sesuai, yang menampilkan data untuk akun tertentu.

Rekaman layar overview (ringkasan), yang men-scroll ke tujuan klik akhir dan mencoba mengklik. Klik sekarang mengarah ke tujuan.

8. Mengaktifkan dukungan deep link

Selain menambahkan argumen, Anda juga dapat menambahkan deep link untuk mengaitkan URL, tindakan, dan/atau jenis mime tertentu dengan composable. Di Android, deep link adalah link yang mengarahkan Anda langsung ke tujuan tertentu dalam aplikasi. Navigation Compose mendukung deep link implisit. Saat deep link implisit dipanggil—misalnya, saat pengguna mengklik link—Android akan dapat membuka aplikasi Anda ke tujuan yang sesuai.

Di bagian ini, Anda akan menambahkan deep link baru untuk membuka composable SingleAccountScreen dengan jenis akun yang sesuai dan memungkinkan deep link ini diekspos ke aplikasi eksternal juga. Untuk memperbarui memori Anda, rute untuk composable ini adalah "single_account/{account_type}" dan rute ini juga yang akan Anda gunakan untuk deep link, dengan beberapa perubahan kecil terkait deep link.

Mengekspos deep link ke aplikasi eksternal tidak diaktifkan secara default. Oleh karena itu, Anda juga harus menambahkan elemen <intent-filter> ke file manifest.xml aplikasi sehingga ini akan menjadi langkah pertama Anda.

Mulai dengan menambahkan deep link ke AndroidManifest.xml aplikasi. Anda harus membuat filter intent baru melalui <intent-filter> di dalam <activity>, dengan tindakan VIEW dan kategori BROWSABLE serta DEFAULT.

Kemudian di dalam filter, Anda memerlukan tag data untuk menambahkan scheme (rally - nama aplikasi) dan host (single_account - arahkan ke composable) untuk menentukan deep link yang tepat. Dengan ini Anda akan mendapatkan rally://single_account sebagai URL deep link.

Perhatikan bahwa Anda tidak perlu mendeklarasikan argumen account_type di AndroidManifest. Argumen ini akan ditambahkan nanti di dalam fungsi composable NavHost.

<activity
    android:name=".RallyActivity"
    android:windowSoftInputMode="adjustResize"
    android:label="@string/app_name"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="rally" android:host="single_account" />
    </intent-filter>
</activity>

Sekarang Anda dapat bereaksi terhadap intent yang masuk dari dalam RallyActivity.

Composable SingleAccountScreen telah menerima argumen, tetapi sekarang juga harus menerima deep link yang baru dibuat untuk meluncurkan tujuan ini saat deep link dipicu.

Di dalam fungsi composable SingleAccountScreen, tambahkan satu parameter deepLinks lagi. Serupa dengan arguments,, class ini juga menerima daftar navDeepLink karena Anda dapat menentukan beberapa deep link yang mengarah ke tujuan yang sama. Teruskan uriPattern agar cocok dengan yang ditentukan dalam intent-filter di manifes Anda - rally://singleaccount, tetapi kali ini Anda juga akan menambahkan argumen accountTypeArg:

import androidx.navigation.navDeepLink
// ...

composable(
    route = SingleAccount.routeWithArgs,
    // ...
    deepLinks = listOf(navDeepLink {
        uriPattern = "rally://${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
    })
)

Anda sudah tahu apa yang akan dilakukan berikutnya, bukan? Pindahkan daftar ini ke RallyDestinations SingleAccount:

object SingleAccount : RallyDestination {
    // ...
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
    val deepLinks = listOf(
       navDeepLink { uriPattern = "rally://$route/{$accountTypeArg}"}
    )
}

Sekali lagi, ganti dalam composable NavHost yang sesuai:

// ...
composable(
    route = SingleAccount.routeWithArgs,
    arguments = SingleAccount.arguments,
    deepLinks = SingleAccount.deepLinks
) {...}

Sekarang aplikasi Anda dan SingleAccountScreen siap menangani deep link. Untuk menguji apakah perilakunya benar, lakukan penginstalan baru Rally pada emulator atau perangkat yang terhubung, buka command line, dan jalankan perintah berikut untuk menyimulasikan peluncuran deep link:

adb shell am start -d "rally://single_account/Checking" -a android.intent.action.VIEW

Tindakan ini akan mengarahkan Anda langsung ke akun "Pemeriksaan", tetapi Anda juga dapat memverifikasi apakah akun berfungsi dengan benar untuk semua jenis akun lainnya.

9. Mengekstrak NavHost ke RallyNavHost

Sekarang NavHost Anda selesai. Namun, agar dapat diuji dan menjaga RallyActivity agar lebih sederhana, Anda dapat mengekstrak NavHost saat ini dan fungsi bantuannya, seperti navigateToSingleAccount, dari composable RallyApp ke fungsi composablenya sendiri dan menamainya RallyNavHost.

RallyApp adalah satu-satunya composable yang akan berfungsi langsung dengan navController. Seperti yang telah disebutkan sebelumnya, setiap layar composable bertingkat lainnya hanya boleh mendapatkan callback navigasi, bukan navController itu sendiri.

Oleh karena itu, RallyNavHost baru akan menerima navController dan modifier sebagai parameter dari RallyApp:

@Composable
fun RallyNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        composable(route = Overview.route) {
            OverviewScreen(
                onClickSeeAllAccounts = {
                    navController.navigateSingleTopTo(Accounts.route)
                },
                onClickSeeAllBills = {
                    navController.navigateSingleTopTo(Bills.route)
                },
                onAccountClick = { accountType ->
                   navController.navigateToSingleAccount(accountType)
                }
            )
        }
        composable(route = Accounts.route) {
            AccountsScreen(
                onAccountClick = { accountType ->
                   navController.navigateToSingleAccount(accountType)
                }
            )
        }
        composable(route = Bills.route) {
            BillsScreen()
        }
        composable(
            route = SingleAccount.routeWithArgs,
            arguments = SingleAccount.arguments,
            deepLinks = SingleAccount.deepLinks
        ) { navBackStackEntry ->
            val accountType =
              navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)
            SingleAccountScreen(accountType)
        }
    }
}

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { launchSingleTop = true }

private fun NavHostController.navigateToSingleAccount(accountType: String) {
    this.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}

Sekarang, tambahkan RallyNavHost baru ke RallyApp dan jalankan kembali aplikasi untuk memverifikasi bahwa semuanya berfungsi seperti sebelumnya:

fun RallyApp() {
    RallyTheme {
    ...
        Scaffold(
        ...
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding)
            )
        }
     }
}

10. Menguji Navigasi Compose

Dari awal codelab ini, Anda telah memastikan untuk tidak meneruskan navController langsung ke composable mana pun (selain aplikasi tingkat tinggi) 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.

Anda harus selalu memastikan seluruh mekanisme Navigation Compose berfungsi sebagaimana mestinya di aplikasi Anda dengan menguji RallyNavHost dan tindakan navigasi yang diteruskan ke composable Anda. Ini akan menjadi sasaran utama bagian ini. Untuk menguji setiap fungsi composable secara terpisah, pastikan Anda melihat codelab Pengujian di Jetpack Compose.

Untuk memulai pengujian, pertama-tama kita harus menambahkan dependensi pengujian yang diperlukan. Jadi, kembalilah ke file build aplikasi Anda yang terdapat di app/build.gradle. Di bagian dependensi, tambahkan dependensi navigation-testing:

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

Mempersiapkan class NavigationTest

RallyNavHost dapat diuji secara terpisah dari Activity itu sendiri.

Pengujian ini akan tetap berjalan di perangkat Android. Oleh karena itu, Anda harus membuat direktori pengujian /app/src/androidTest/java/com/example/compose/rally, lalu membuat class pengujian file pengujian baru dan menamainya NavigationTest.

Sebagai langkah pertama, untuk menggunakan API pengujian Compose, serta menguji dan mengontrol composable dan aplikasi menggunakan Compose, tambahkan aturan pengujian Compose:

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

}

Menulis pengujian pertama

Buat fungsi pengujian rallyNavHost publik dan anotasikan dengan @Test. Dalam fungsi tersebut, Anda harus terlebih dahulu menetapkan konten Compose yang ingin diuji. Lakukan hal ini menggunakan setContent milik composeTestRule. Composable ini memerlukan parameter composable sebagai isi serta memungkinkan Anda menulis kode Compose dan menambahkan composable di lingkungan pengujian, seolah-olah Anda berada di aplikasi lingkungan produksi reguler.

Di dalam setContent,, Anda dapat menyiapkan subjek pengujian saat ini, RallyNavHost dan meneruskan instance navController baru ke instance tersebut. Artefak pengujian Navigasi menyediakan TestNavHostController yang praktis untuk digunakan. Jadi, mari kita tambahkan langkah ini:

import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
import org.junit.Assert.fail
import org.junit.Test
// ...

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    lateinit var navController: TestNavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            // Creates a TestNavHostController
            navController =
                TestNavHostController(LocalContext.current)
            // Sets a ComposeNavigator to the navController so it can navigate through composables
            navController.navigatorProvider.addNavigator(
                ComposeNavigator()
            )
            RallyNavHost(navController = navController)
        }
        fail()
    }
}

Jika Anda menyalin kode di atas, panggilan fail() akan memastikan bahwa pengujian Anda gagal hingga ada pernyataan sebenarnya yang dibuat. Hal ini berfungsi sebagai pengingat untuk menyelesaikan penerapan pengujian.

Untuk memverifikasi composable layar yang benar ditampilkan, Anda dapat menggunakan contentDescription dan menegaskan bahwa composable tersebut ditampilkan. Dalam codelab ini, contentDescription untuk tujuan Akun dan Ringkasan telah ditetapkan sehingga Anda sudah dapat menggunakannya untuk verifikasi pengujian.

Sebagai verifikasi pertama, Anda harus memeriksa bahwa layar Ringkasan ditampilkan sebagai tujuan pertama saat RallyNavHost diinisialisasi untuk pertama kalinya. Anda juga harus mengganti nama pengujian untuk mencerminkannya - beri nama rallyNavHost_verifyOverviewStartDestination. Lakukan hal ini dengan mengganti panggilan fail() dengan kode berikut:

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
// ...

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    lateinit var navController: TestNavHostController

    @Test
    fun rallyNavHost_verifyOverviewStartDestination() {
        composeTestRule.setContent {
            navController =
                TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(
                ComposeNavigator()
            )
            RallyNavHost(navController = navController)
        }

        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

Jalankan pengujian lagi, dan pastikan pengujian berhasil.

Anda harus menyiapkan RallyNavHost dengan cara yang sama untuk setiap pengujian selanjutnya. Oleh karena itu, Anda dapat mengekstrak inisialisasinya ke dalam fungsi @Before yang dianotasi untuk menghindari pengulangan yang tidak perlu dan membuat pengujian Anda lebih ringkas:

import org.junit.Before
// ...

class NavigationTest {

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

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

    @Test
    fun rallyNavHost_verifyOverviewStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

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.

Pengujian melalui klik UI dan konten deskripsi layar

Sebaiknya klik UI karena Anda ingin menguji penerapan aplikas yang konkret. Teks berikutnya dapat memverifikasi bahwa, saat berada di layar Ringkasan, mengklik tombol "SEE ALL" di subbagian Akun akan mengarahkan Anda ke tujuan Akun:

5a9e82acf7efdd5b.png

Anda akan kembali menggunakan contentDescription yang telah ditetapkan pada tombol khusus ini dalam composable OverviewScreenCard, yang menyimulasikan klik melalui performClick() dan memverifikasi bahwa tujuan Akun akan ditampilkan kemudian:

import androidx.compose.ui.test.performClick
// ...

@Test
fun rallyNavHost_clickAllAccount_navigatesToAccounts() {
    composeTestRule
        .onNodeWithContentDescription("All Accounts")
        .performClick()

    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

Anda dapat mengikuti pola ini untuk menguji semua tindakan navigasi klik yang tersisa di aplikasi.

Pengujian melalui klik UI dan perbandingan rute

Anda juga dapat menggunakan navController untuk memeriksa pernyataan dengan membandingkan rute String saat ini dengan yang diharapkan. Untuk melakukannya, klik UI, seperti yang ada di bagian sebelumnya, lalu bandingkan rute saat ini dengan yang Anda harapkan menggunakan navController.currentBackStackEntry?.destination?.route.

Salah satu langkah tambahan adalah memastikan Anda men-scroll terlebih dahulu ke subbagian Tagihan di layar Ringkasan, jika tidak, pengujian akan gagal karena tidak dapat menemukan node dengan contentDescription "All Bills":

import androidx.compose.ui.test.performScrollTo
import org.junit.Assert.assertEquals
// ...

@Test
fun rallyNavHost_clickAllBills_navigateToBills() {
    composeTestRule.onNodeWithContentDescription("All Bills")
        .performScrollTo()
        .performClick()

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

Dengan mengikuti pola ini, Anda dapat menyelesaikan class pengujian dengan mencakup rute navigasi tambahan, tujuan, dan tindakan klik. Jalankan seluruh rangkaian pengujian sekarang untuk memastikan semuanya lulus.

11. Selamat

Selamat, Anda berhasil menyelesaikan codelab ini. Anda dapat menemukan kode solusi di sini dan membandingkannya dengan kode Anda.

Anda telah menambahkan navigasi Jetpack Compose ke aplikasi Rally dan kini telah memahami konsep utamanya. Anda telah mempelajari cara menyiapkan grafik navigasi tujuan composable, menentukan rute dan tindakan navigasi, meneruskan informasi tambahan ke rute melalui argumen, menyiapkan deep link, dan menguji navigasi.

Untuk topik dan informasi lainnya, misalnya integrasi menu navigasi bawah, navigasi multi-modul dan grafik bertingkat, Anda dapat membaca Sekarang di repositori GitHub Android dan melihat cara penerapannya di sana.

Apa selanjutnya?

Lihat materi ini untuk melanjutkan jalur pembelajaran Jetpack Compose:

Informasi lengkap Navigasi Jetpack:

Dokumen referensi