Skip to Content
Course content

196: Practice Exercise: Building a Type-Safe Navigation Graph

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen a dozen times: a developer switches to type-safe navigation and thinks, "Great, now I just use a data class instead of a String, and I've solved my routing problems." They treat the route object like a simple container, but they still try to manually parse arguments or use navArgument blocks in their destination definitions. They think the "type-safety" is just a fancy way of avoiding typos in a URL string.

Here is why that's wrong. If you're still manually defining arguments in your graph, you aren't actually using type-safe navigation; you're just using a wrapper. The whole point of the modern Kotlin Navigation approach is to move the source of truth from the NavGraph definition to the Serializable class itself. When you do it the old way, you're still duplicating your schema in two places: the class you use to navigate and the composable definition where you declare the arguments. That's just more code to keep in sync.

Thinking Type-Safe Navigation is Just String Constants in Disguise

In the old days (which was basically last year), we did this:

val route = "details/{movieId}"
navController.navigate("details/$movieId")

// In the graph
composable(route) { backStackEntry ->
    val movieId = backStackEntry.arguments?.getString("movieId")
    MovieDetailsScreen(movieId)
}

Even if you put "details/{movieId}" into a constant, it's still a String. If you change the argument name in the graph but forget to change it in the navigate call, the app compiles perfectly and then crashes at runtime. I can't tell you how many hours I've wasted hunting down a misplaced slash or a misspelled argument key in a complex graph.

Letting the Compiler Own the Route Schema

The correct way is to let Kotlin Serialization handle the mapping. You define a @Serializable object or class, and that is the route. There is no String involved in your business logic.

Let's look at a real-world example: a movie app where you need to pass a movie ID and a user's preferred playback quality.

@Serializable
data class MovieDetails(val movieId: String, val quality: String)

// Navigating is now a type-safe function call
navController.navigate(MovieDetails(movieId = "m123", quality = "4K"))

// Defining the destination
composable<MovieDetails> { backStackEntry ->
    val args = backStackEntry.toRoute<MovieDetails>()
    MovieDetailsScreen(args.movieId, args.quality)
}

Notice what happened here. I didn't define a path. I didn't specify that movieId is a String. The toRoute<T>() function uses the generic type to automatically extract the arguments from the navigation bundle. If I add a userId to the MovieDetails class, the code will literally refuse to compile until I update every single navigate call in the entire app. That is the power of moving the logic from runtime to compile-time.

One quick tip: keep your route classes lean. Don't pass huge data objects or complex custom classes through the navigation graph. Stick to IDs and primitives. If you need a full User object, pass the userId and let the destination screen fetch the data from a repository or a shared ViewModel. It keeps your navigation state small and prevents TransactionTooLargeException crashes.




📋 Practical Task

Exercise: Implementing a Type-Safe User Profile and Settings Flow

You are building a User Management module. You need to implement a type-safe navigation flow between a UserProfile screen and a UserSettings screen.

Requirements:

  • Create a @Serializable data class for UserProfile that accepts a userId: String and an isAdmin: Boolean.
  • Create a @Serializable data class for UserSettings that accepts a userId: String and a section: String (e.g., "Privacy", "Notifications").
  • In your NavHost, define the two composable destinations using these types.
  • Inside the UserProfile destination, implement a button that navigates to the UserSettings screen, passing the userId from the current route and the string "Privacy" as the section.
  • Ensure you use toRoute<T>() to retrieve the arguments in both screens.

Note: Assume the necessary Navigation and Kotlin Serialization dependencies are already configured in the project.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.