The Cost of String-Based Routes
Defining Routes as Kotlin Types
import kotlinx.serialization.Serializable
// Simple destination with no arguments
@Serializable
object Home
// Destination with required arguments
@Serializable
data class ProductDetail(val productId: String)
// Destination with optional arguments
@Serializable
data class Search(
val query: String = "",
val category: String? = null,
val minPrice: Int = 0,
val maxPrice: Int = Int.MAX_VALUE
)
// Nested graph parent
@Serializable
object AuthGraph
@Serializable
object Login
@Serializable
data class ResetPassword(val email: String)Building the NavHost with Type-Safe DSL
@Composable
fun AppNavHost(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Home
) {
composable<Home> {
HomeScreen(
onProductClick = { productId ->
navController.navigate(ProductDetail(productId))
},
onSearchClick = {
navController.navigate(Search())
}
)
}
composable<ProductDetail> { backStackEntry ->
val route = backStackEntry.toRoute<ProductDetail>()
ProductDetailScreen(
productId = route.productId,
onBack = { navController.popBackStack() }
)
}
composable<Search> { backStackEntry ->
val route = backStackEntry.toRoute<Search>()
SearchScreen(
initialQuery = route.query,
initialCategory = route.category,
priceRange = route.minPrice..route.maxPrice
)
}
// Nested auth graph
navigation<AuthGraph>(startDestination = Login) {
composable<Login> {
LoginScreen(
onForgotPassword = { email ->
navController.navigate(ResetPassword(email))
}
)
}
composable<ResetPassword> { entry ->
val route = entry.toRoute<ResetPassword>()
ResetPasswordScreen(email = route.email)
}
}
}
}Deep Links with Type Safety
composable<ProductDetail>(
deepLinks = listOf(
navDeepLink<ProductDetail>(
basePath = "https://myapp.com/product"
)
)
) { backStackEntry ->
val route = backStackEntry.toRoute<ProductDetail>()
ProductDetailScreen(productId = route.productId)
}
// Custom NavType for enums
@Serializable
data class OrderList(val status: OrderStatus = OrderStatus.ALL)
@Serializable
enum class OrderStatus { ALL, PENDING, SHIPPED, DELIVERED }
// The URL https://myapp.com/orders?status=SHIPPED
// automatically deserializes to OrderList(status = OrderStatus.SHIPPED)Testing Type-Safe Navigation
class FakeNavigator {
val navigatedRoutes = mutableListOf<Any>()
inline fun <reified T : Any> navigate(route: T) {
navigatedRoutes.add(route)
}
inline fun <reified T : Any> assertNavigatedTo(
expected: T
) {
val last = navigatedRoutes.lastOrNull()
assertIs<T>(last)
assertEquals(expected, last)
}
}
@Test
fun `tapping product navigates to detail with correct ID`() {
val navigator = FakeNavigator()
val viewModel = HomeViewModel(navigator, fakeRepo)
viewModel.onProductClick("SKU-42")
navigator.assertNavigatedTo(
ProductDetail(productId = "SKU-42")
)
}Migration Strategy from String Routes
Key Takeaways
- 1String-based navigation causes 14% of non-ANR crashes in production Compose apps -- type-safe routes reduce this to under 1%.
- 2Define routes as @Serializable data classes and objects for compile-time argument validation.
- 3Use composable<T> DSL instead of composable(route = "...") for self-documenting NavHost graphs.
- 4Deep links automatically validate against your route types, rejecting malformed input gracefully.
- 5Type-safe and string-based routes coexist, enabling incremental migration without destabilization.
- 6Testing becomes simple data class comparison instead of brittle string assertions.
Frequently Asked
Is type-safe navigation worth the complexity?
Yes for medium to large apps. The compile-time safety prevents runtime crashes and makes refactoring safe. For tiny apps, string routes may suffice.
Does this work with bottom navigation?
Yes. Map bottom nav items to sealed class routes. Use current destination to highlight active tab. Type-safe navigation integrates with all Navigation Compose features.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.