Introduction

Navigation Compose passes arguments between screens through the navigation route string and SavedStateHandle. When arguments are lost, become null, or fail to parse, the destination screen crashes or shows blank data. Common causes include mismatched argument type declarations, missing default values for optional arguments, deep links that do not properly encode arguments, and complex objects passed as arguments that are not serializable. Navigation Compose only supports a limited set of argument types (Int, Long, Float, Boolean, String) by default, requiring custom NavType for other types.

Symptoms

  • Destination screen receives null for required arguments
  • IllegalArgumentException: Argument is null in navigation
  • Complex objects passed as arguments are lost on process death
  • Deep link opens screen but arguments are empty
  • Navigation works in debug but fails in release builds
  • SavedStateHandle.get<T>() returns wrong type

Common Causes

  • Route argument type does not match passed value type
  • Required argument not provided in navigation route
  • Complex object passed as String argument not serialized/deserialized
  • Deep link URL does not include query parameters in correct format
  • ProGuard/R8 stripping navigation argument classes in release builds

Step-by-Step Fix

  1. 1.Use type-safe navigation with proper argument declaration:
  2. 2.```kotlin
  3. 3.// Define navigation routes with type-safe arguments
  4. 4.sealed class Screen(val route: String) {
  5. 5.data object Home : Screen("home")
  6. 6.data object UserDetail : Screen("user/{userId}") {
  7. 7.fun createRoute(userId: Int) = "user/$userId"
  8. 8.}
  9. 9.data object OrderDetail : Screen("order/{orderId}?status={status}") {
  10. 10.fun createRoute(orderId: String, status: String = "pending") =
  11. 11."order/$orderId?status=$status"
  12. 12.}
  13. 13.}

// Navigation setup NavHost(navController = navController, startDestination = Screen.Home.route) { composable(Screen.Home.route) { HomeScreen( onUserClick = { userId -> navController.navigate(Screen.UserDetail.createRoute(userId)) } ) }

composable( route = Screen.UserDetail.route, arguments = listOf( navArgument("userId") { type = NavType.IntType } ) ) { backStackEntry -> val userId = backStackEntry.arguments?.getInt("userId") ?: throw IllegalArgumentException("userId is required") UserDetailScreen(userId = userId) }

composable( route = Screen.OrderDetail.route, arguments = listOf( navArgument("orderId") { type = NavType.StringType }, navArgument("status") { type = NavType.StringType defaultValue = "pending" } ) ) { backStackEntry -> val orderId = backStackEntry.arguments?.getString("orderId")!! val status = backStackEntry.arguments?.getString("status") ?: "pending" OrderDetailScreen(orderId, status) } } ```

  1. 1.Pass complex objects via SavedStateHandle or serialization:
  2. 2.```kotlin
  3. 3.// WRONG - cannot pass complex objects directly
  4. 4.navController.navigate("user/${userObject}") // Uses toString(), loses data

// CORRECT - pass only the ID, fetch data in destination navController.navigate(Screen.UserDetail.createRoute(user.id))

// OR serialize to JSON for small objects val userJson = Json.encodeToString(UserSerializer, user) val encoded = URLEncoder.encode(userJson, "UTF-8") navController.navigate("user_detail/$encoded")

// Destination - deserialize composable("user_detail/{user}") { backStackEntry -> val userJson = URLDecoder.decode( backStackEntry.arguments?.getString("user") ?: "", "UTF-8" ) val user = Json.decodeFromString<User>(userJson) UserDetailScreen(user) }

// For large objects, pass ID and use Shared ViewModel class SharedViewModel : ViewModel() { private val _selectedUser = MutableStateFlow<User?>(null) val selectedUser: StateFlow<User?> = _selectedUser

fun selectUser(user: User) { _selectedUser.value = user } }

// Source screen sharedViewModel.selectUser(user) navController.navigate(Screen.UserDetail.route)

// Destination screen val user by sharedViewModel.selectedUser.collectAsState() if (user == null) { // Fallback: fetch by ID from repository } ```

  1. 1.Handle deep link arguments correctly:
  2. 2.```kotlin
  3. 3.composable(
  4. 4.route = "product/{productId}",
  5. 5.arguments = listOf(
  6. 6.navArgument("productId") {
  7. 7.type = NavType.StringType
  8. 8.nullable = false
  9. 9.}
  10. 10.),
  11. 11.deepLinks = listOf(
  12. 12.navDeepLink {
  13. 13.uriPattern = "https://myapp.com/product/{productId}"
  14. 14.}
  15. 15.)
  16. 16.) { backStackEntry ->
  17. 17.val productId = backStackEntry.arguments?.getString("productId")
  18. 18.ProductScreen(productId = productId ?: return@composable)
  19. 19.}

// Deep link with query parameters composable( route = "search?query={query}&category={category}", arguments = listOf( navArgument("query") { type = NavType.StringType defaultValue = "" }, navArgument("category") { type = NavType.StringType defaultValue = "all" } ), deepLinks = listOf( navDeepLink { uriPattern = "https://myapp.com/search?query={query}&category={category}" } ) ) { backStackEntry -> val query = backStackEntry.arguments?.getString("query") ?: "" val category = backStackEntry.arguments?.getString("category") ?: "all" SearchScreen(query, category) } ```

  1. 1.**Use Navigation 2.8+ type-safe APIs":
  2. 2.```kotlin
  3. 3.// Navigation 2.8+ introduces compile-time safe navigation
  4. 4.@Serializable
  5. 5.object Home

@Serializable data class UserDetail(val userId: Int)

@Serializable data class OrderDetail(val orderId: String, val status: String = "pending")

// Setup val navController = rememberNavController() NavHost(navController, startDestination = Home) { composable<Home> { HomeScreen(onUserClick = { userId -> navController.navigate(UserDetail(userId)) }) }

composable<UserDetail> { backStackEntry -> val userDetail = backStackEntry.toRoute<UserDetail>() UserDetailScreen(userId = userDetail.userId) }

composable<OrderDetail> { backStackEntry -> val orderDetail = backStackEntry.toRoute<OrderDetail>() OrderDetailScreen(orderDetail.orderId, orderDetail.status) } } ```

Prevention

  • Pass IDs instead of complex objects — fetch full data in the destination
  • Use type-safe navigation routes with factory methods to prevent typos
  • Define default values for optional arguments
  • Test navigation with process death (developer options > Don't keep activities)
  • Use Navigation 2.8+ @Serializable APIs for compile-time safety
  • Encode URL parameters when passing strings that may contain special characters
  • Verify deep links work with adb shell am start command