Compose Navigation adalah topik penting dalam pengembangan aplikasi Android dengan Kotlin. Memahaminya membantu kita membangun aplikasi yang andal, mudah diuji, dan siap untuk publikasi ke Google Play Store. Pada chapter ini kita akan membongkar konsep inti, melihat contoh kode praktis, dan menutup dengan checklist implementasi.
Komponen Utama #
Navigation Compose menyediakan rememberNavController(), NavHost, dan composable(route). NavController mengelola back stack. NavHost mendefinisikan graph route → composable.
Berikut program demonstrasi konsep-konsep di atas:
// compose_navigation.kt
// Mensimulasikan NavHost, composable("route"), back stack, dan bottom
// navigation bar sebagai state machine ringkas. Mendemonstrasikan type-safe
// args via data class untuk setiap route.
data class Route(val path: String, val argKeys: List<String> = emptyList()) {
fun build(vararg args: Any): String {
require(args.size == argKeys.size) { "Expected ${argKeys.size} args, got ${args.size}" }
var built = path
args.forEachIndexed { i, value ->
built = built.replace("{${argKeys[i]}}", value.toString())
}
return built
}
}
object Routes {
val home = Route("home")
val detail = Route("detail/{id}", listOf("id"))
val settings = Route("settings")
}
class NavHost(val startDestination: String) {
private val backStack = ArrayDeque<String>()
val currentRoute: String? get() = backStack.lastOrNull()
init {
backStack.addLast(startDestination)
}
fun navigate(route: String) {
backStack.addLast(route)
println(" navigate -> $route")
}
fun popBackStack(): Boolean {
if (backStack.size <= 1) return false
backStack.removeLast()
println(" popBackStack -> ${backStack.lastOrNull()}")
return true
}
fun stackSize(): Int = backStack.size
fun dump(): List<String> = backStack.toList()
}
class BottomNavigationBar(val navHost: NavHost) {
private val tabs = listOf("home" to "Beranda", "settings" to "Pengaturan")
fun selectTab(route: String) {
if (route != navHost.currentRoute) {
navHost.navigate(route)
}
}
fun render(): String = tabs.joinToString(" | ") { (route, label) ->
val marker = if (route == navHost.currentRoute) "[$label]" else label
marker
}
}
fun main() {
val nav = NavHost(startDestination = Routes.home.path)
println("Initial: ${nav.currentRoute}")
val detailRoute = Routes.detail.build(42)
nav.navigate(detailRoute)
nav.navigate(Routes.settings.path)
println("Back stack (${nav.stackSize()}):")
nav.dump().forEach { println(" - $it") }
val bar = BottomNavigationBar(nav)
println("Bottom bar: ${bar.render()}")
bar.selectTab("home")
println("Setelah pilih Beranda: current=${nav.currentRoute}")
while (nav.popBackStack()) { /* pop sampai root */ }
println("Stack akhir: ${nav.dump()}")
}Output yang Dihasilkan:
Initial: home
navigate -> detail/42
navigate -> settings
Back stack (3):
- home
- detail/42
- settings
Bottom bar: Beranda | [Pengaturan]
navigate -> home
Setelah pilih Beranda: current=home
popBackStack -> settings
popBackStack -> detail/42
popBackStack -> home
Stack akhir: [home]Route dan Argumen #
Route adalah string identifier: "home", "detail/{id}". Argumen dideklarasikan dengan arguments = listOf(navArgument("id") { type = NavType.LongType }). Type-safe navigation Kotlin DSL (1.0+) menyediakan compile-time type check.
Bottom Navigation #
NavHost + NavigationBar dengan item yang highlight sesuai current route. Pattern: expose currentBackStackEntryAsState() untuk membaca route aktif.
Nested Navigation #
Navigation graph bersarang untuk fitur kompleks: navigation(startDestination = "home", route = "main"). Memungkinkan satu route membuka seluruh graph.
Deep Link #
Tambahkan composable(...) { deepLinks = listOf(navDeepLink { uriPattern = "myapp://detail/{id}" }) }. Memungkinkan URL eksternal membuka screen tertentu.
Type-Safe Navigation (1.0+) #
Navigation Compose 1.0 ke atas menyediakan DSL type-safe yang menggantikan string route manual. Setiap destinasi direpresentasikan sebagai data class yang dianotasi @Serializable melalui kotlinx.serialization. Kompilator memeriksa tipe argumen saat compile-time, sehingga kesalahan seperti mengirim String ke parameter Int akan langsung muncul sebagai error build.
@Serializable data class HomeRoute(val tab: String = "feed")
@Serializable data class DetailRoute(val itemId: Long)
NavHost(navController, startDestination = HomeRoute()) {
composable<HomeRoute> { HomeScreen() }
composable<DetailRoute> { backStackEntry ->
val args = backStackEntry.toRoute<DetailRoute>()
DetailScreen(itemId = args.itemId)
}
}
// Navigasi:
navController.navigate(DetailRoute(itemId = 42))Keuntungan utama pola ini adalah eliminasi boilerplate navArgument yang sebelumnya perlu didefinisikan manual. Kompilator juga menjamin setiap argumen wajib terisi pada saat pemanggilan navigate, sehingga crash akibat argumen hilang terdeteksi sebelum runtime. Untuk tipe kompleks seperti List<Long>, sediakan NavType kustom yang mengimplementasikan serialisasi melalui kotlinx.serialization.
Type-safe navigation juga memudahkan deep link. Cukup tambahkan deepLinks = listOf(navDeepLink { uriPattern = "myapp://detail/{itemId}" }) pada deklarasi composable, dan kompilator mencocokkan parameter berdasarkan nama yang sama di data class. Refactor nama field otomatis terdeteksi lintas pemanggilan tanpa perlu mencari string secara manual.
Bottom Navigation Pattern #
Pola bottom navigation yang lazim digunakan menggabungkan NavHost dan NavigationBar dalam satu Scaffold. Tugas utamanya adalah menjaga sinkronisasi tab yang aktif dengan back stack entry teratas. Compose Navigation menyediakan currentBackStackEntryAsState() untuk membaca route aktif tanpa rekomposisi berlebihan pada seluruh Scaffold.
val navController = rememberNavController()
val items = listOf(
BottomItem("home", "Beranda", Icons.Default.Home),
BottomItem("settings", "Pengaturan", Icons.Default.Settings)
)
Scaffold(bottomBar = {
val backStack by navController.currentBackStackEntryAsState()
val currentRoute = backStack?.destination?.route
NavigationBar {
items.forEach { item ->
NavigationBarItem(
selected = currentRoute == item.route,
onClick = {
if (currentRoute != item.route) {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
},
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) }
)
}
}
}) { padding ->
NavHost(navController, startDestination = "home", Modifier.padding(padding)) {
composable("home") { HomeScreen() }
composable("settings") { SettingsScreen() }
}
}Tiga opsi penting pada navigate menjaga perilaku tab konsisten: popUpTo membersihkan back stack sampai ke start destination, launchSingleTop mencegah duplikat route, dan restoreState mengembalikan state scroll saat pengguna berpindah tab. Tanpa kombinasi ini, pengguna akan kehilangan posisi scroll setiap kali pindah tab, yang menurunkan kualitas pengalaman pengguna.
Pola ini juga menjaga state ViewModel pada setiap tab tetap hidup. Saat pengguna kembali ke tab sebelumnya, data yang sudah dimuat tidak perlu dimuat ulang. Untuk layar utama yang berat seperti feed atau dashboard, perilaku ini sangat penting menjaga pengalaman pengguna tetap responsif. Bila butuh navigasi lebih kompleks dengan banyak tab, pertimbangkan nested NavHost agar setiap tab memiliki back stack mandiri seperti pola pada aplikasi media sosial besar.
Untuk melengkapi pola ini, pasang NavController.addOnDestinationChangedListener di MainActivity agar setiap transisi route tercatat di analytics. Callback ini menerima destination.route yang bisa dikirim ke Firebase Analytics atau layanan tracking lainnya. Pastikan untuk tidak menyertakan data sensitif seperti ID pengguna sebagai bagian dari nama event, melainkan simpan sebagai parameter terpisah yang sudah dihash.
Referensi Lanjutan #
- Jetpack Compose — rujukan Jetpack Compose untuk integrasi komponen Navigation
- Navigation — panduan resmi komponen Navigation Android Jetpack sebagai dasar nav graph Compose
Penutup #
Dengan memahami compose navigation, kita siap membangun aplikasi Android yang lebih matang dan siap produksi. Materi terkait di section lain meliputi coroutines, Compose Multiplatform, dan testing — semuanya saling melengkapi untuk pengembangan Android modern.