Jetpack Compose में Type-Safe Navigation: Routes को Strings नहीं, Data की तरह
String-based Compose Navigation routes को serializable Kotlin objects से कैसे बदलें — type-safe arguments, nested graphs, और deep links जो refactor के बाद भी टिके रहें।
Compose Navigation के शुरुआती कुछ सालों में, हर route एक string होता था। आप "item/{itemId}" बनाते, उसे किसी NavHost में match करते, और itemId को वापस किसी NavBackStackEntry से getString("itemId") के ज़रिए निकालते — यह उम्मीद करते हुए कि जो key आपने भेजते वक्त टाइप की थी वह उस key से मेल खाए जो आपने पाते वक्त टाइप की थी। कंपाइलर यहाँ आपकी कोई मदद नहीं कर सकता था। किसी भी string में टाइपो पूरी तरह compile हो जाता और runtime पर crash करता, आमतौर पर उस स्क्रीन पर जिसे आप बदलाव करते वक्त देख भी नहीं रहे होते। Navigation Compose के type-safe routes, route को string की बजाय एक Kotlin object बनाकर इसे ठीक करते हैं, और किसी shipping app की हर स्क्रीन को इस पर ले जाने के बाद, argument-typo वाली बग की पूरी क्लास पूरी तरह ख़त्म हो जाती है — कंपाइलर इसे build पूरा होने से पहले ही पकड़ लेता है।
एक sealed hierarchy के रूप में Routes
मूल विचार छोटा है: अपने destinations को एक sealed interface के रूप में परिभाषित करें, हर एक को @Serializable से annotate करें, और उन्हें route में बदलने और वापस लाने का काम kotlinx.serialization को करने दें।
sealed interface Screen {
@Serializable
data object Home : Screen
@Serializable
data class ItemDetail(val itemId: Long) : Screen
@Serializable
data class EditItem(val itemId: Long, val fromDetail: Boolean = false) : Screen
}
कोई string template नहीं, कोई key नाम sync में रखने की ज़रूरत नहीं — arguments ही class की properties हैं। Navigate करना असली types वाले एक function call जैसा है:
navController.navigate(Screen.ItemDetail(itemId = item.id))
अगर कहीं ऊपर itemId का नाम बदल दिया जाए या इसका type Long से String में बदल जाए, तो हर call site runtime पर parse फ़ेल होने की बजाय compile होने में ही फ़ेल हो जाएगी।
NavHost को वायर करना
NavHost वाला हिस्सा इसे composable<T>() overload के साथ प्रतिबिंबित करता है, जो string pattern की बजाय type को एक reified parameter के रूप में लेता है:
NavHost(navController = navController, startDestination = Screen.Home) {
composable<Screen.Home> {
HomeScreen(onItemClick = { id -> navController.navigate(Screen.ItemDetail(id)) })
}
composable<Screen.ItemDetail> { backStackEntry ->
val args: Screen.ItemDetail = backStackEntry.toRoute()
ItemDetailScreen(itemId = args.itemId)
}
composable<Screen.EditItem> { backStackEntry ->
val args: Screen.EditItem = backStackEntry.toRoute()
EditItemScreen(itemId = args.itemId, cameFromDetail = args.fromDetail)
}
}
toRoute() back stack entry को सीधे destination type में deserialize कर देता है। न कोई Bundle key है जिसकी स्पेलिंग गलत हो सकती है, न किसी missing argument पर manual null-check की ज़रूरत है — जो required property मौजूद नहीं है वह बस compile ही नहीं होती, और जो optional है उसे अपना declared default मिल जाता है, बिल्कुल किसी भी अन्य Kotlin function argument की तरह।
Nested graphs भी type-safe रहते हैं
यही पैटर्न nested navigation graphs तक भी फैलता है, जहाँ string routes असल में सबसे ज़्यादा तकलीफ़देह हुआ करते थे — किसी nested graph के start destination में टाइपो चुपचाप फ़ेल हो जाता है और बस एक खाली स्क्रीन दिखा देता है। Sealed routes के साथ, nested graph अपना ही एक type होता है:
sealed interface OnboardingGraph : Screen {
@Serializable data object Welcome : OnboardingGraph
@Serializable data object Permissions : OnboardingGraph
@Serializable data class Done(val skippedPermissions: Boolean) : OnboardingGraph
}
navigation<OnboardingGraph>(startDestination = OnboardingGraph.Welcome) {
composable<OnboardingGraph.Welcome> { /* ... */ }
composable<OnboardingGraph.Permissions> { /* ... */ }
composable<OnboardingGraph.Done> { entry ->
val args: OnboardingGraph.Done = entry.toRoute()
// ...
}
}
चूँकि OnboardingGraph, Screen को extend करता है, app में कहीं और मौजूद वह कोड जो बाहरी graph के खिलाफ़ navigate करता है, उसे यह जानने की ज़रूरत नहीं कि nested graph मौजूद है — वह बस navigate(OnboardingGraph.Welcome) कॉल करता है और graph की सीमा structurally resolve होती है, न कि string prefix matching से।
Deep links, बिना किसी गलत हो सकने वाले URI template के
Deep links उन्हीं typed destinations से जुड़ते हैं, तो एक आने वाला URI अब भी हाथ से parse करने वाले strings के बैग की बजाय एक असली object में resolve होता है:
composable<Screen.ItemDetail>(
deepLinks = listOf(navDeepLink<Screen.ItemDetail>(basePath = "myapp://item"))
) { backStackEntry ->
val args: Screen.ItemDetail = backStackEntry.toRoute()
ItemDetailScreen(itemId = args.itemId)
}
myapp://item/42 और एक in-app navigate(Screen.ItemDetail(42)), दोनों एक ही composable में एक ही typed argument के साथ पहुँचते हैं — बिल्कुल एक ही जगह है जो तय करती है कि ItemDetail destination कैसा दिखता है, चाहे वहाँ notification tap से पहुँचे हों, किसी widget से, या app के अंदर किसी बटन से।
Migrate करने से पहले जानने लायक दो बातें
किसी route में custom types को अपने ख़ुद के NavType की ज़रूरत होती है। Long या String argument बिना किसी अतिरिक्त काम के चल जाता है, लेकिन कोई enum या value class रखने वाले route को composable call पर registered एक छोटे NavType<T> implementation की ज़रूरत होती है, क्योंकि back stack आख़िरकार route को एक serialized string के रूप में स्टोर करता है और उसे यह जानना होता है कि आपके type को इसमें encode और decode कैसे करना है। यह कुछ ही लाइनें हैं, लेकिन मुफ़्त नहीं हैं — पहली उस स्क्रीन के लिए इसका बजट रखें जिसे non-primitive argument की ज़रूरत हो।
Leaf-first migrate करें, root-first नहीं। सबसे भीतरी स्क्रीन्स को उन graphs से पहले बदलना जो उन्हें host करते हैं, आपको आगे बढ़ते हुए string और type-safe routes को साथ-साथ चलाने देता है, बजाय एक big-bang rewrite के जिसमें जब तक हर route पूरा न हो तब तक कुछ भी compile नहीं होता। मैंने एक बार में एक flow बदला — पहले onboarding, फिर item detail, फिर settings — हर बार के बाद navigation को हाथ से जाँचते हुए, और कभी भी मुट्ठी भर से ज़्यादा फ़ाइलें असंगत स्थिति में नहीं रहीं।
नतीजा तेज़ navigation कोड नहीं है, बल्कि ऐसा navigation कोड है जहाँ वह बग की क्लास — एक renamed argument जो अब भी गलत type के रूप में parse होता था और code review में बचा रह जाता था — अब बस मौजूद ही नहीं है। अकेले maintain किए जाने वाले किसी app के लिए, जहाँ टाइपो पकड़ने के लिए कोई दूसरा reviewer नहीं है, यही सबसे मायने रखने वाली दलील है।
// संबंधित पठन
जर्नल से और भी
Android का In-App Review API: बिना परेशान किए रेटिंग माँगना
Google के In-App Review API की एक व्यावहारिक गाइड — यह असल में कैसे काम करता है, इसे कब ट्रिगर करना चाहिए, और सामान्य 'हमें रेट करें' पॉपअप आपकी Play Store रेटिंग को चुपचाप कैसे नुकसान पहुँचा रहा है।
Android ऐप शॉर्टकट्स और Quick Settings Tiles: बिना ऐप खोले पानी लॉग करना
Android के डायनामिक ShortcutManager API और TileService की एक व्यावहारिक गाइड — कैसे एक-टैप एक्शन को पूरी तरह ऐप को छोड़कर काम करने दें, साथ ही वे गड़बड़ियाँ जो ज़्यादातर इम्प्लीमेंटेशन को फँसा देती हैं।
2026 में Android पर फोरग्राउंड सर्विस टाइप्स: वह चुनना जो आपके फीचर पर सच में लागू हो
Android के फोरग्राउंड सर्विस टाइप प्रतिबंधों की एक व्यावहारिक गाइड — dataSync, mediaPlayback, specialUse, shortService — और बिना मारे या रिजेक्ट हुए सही वाला कैसे चुनें।