Type-Safe Navigation Jetpack Compose

Benidict Dulce
4 min readJun 19, 2024

--

Kotlin DSL is one way to build the navigation graph since 2018. This API allows you to declaratively compose your graph in your Kotlin code, rather than inside an XML resource.

When we transition to Jetpack Compose, We use Navigation Compose API to navigate between composables “while taking advantage of the Navigation component’s infrastructure and features”. It’s sounds great but the new navigation is string-based navigation in general and the screen route is looks like an url. Each parameter must be String and encoded.

Everything is a String!

For you to observe, I will show you how the compose navigation looks like before the version 2.8.0 .

Navigation Component has 5 concepts:

  • Host — the UI element in your layout that displays the current ‘destination’
  • Graph — the data structure that defines all of the possible destinations in your app
  • Controller — the central coordinator that manages navigating between destinations and saving the back stack of destinations
  • Destination — A node in the navigation graph. When the user navigates to this node, the host displays its content.
  • Route — Uniquely identifies a destination and any data required by it.

First defined the routes in enum class, you can also use the routes directly to the graph as string.

enum class Screen {
TEAM,
GAMES
}

Second create enum for your graph, you can also use the graph directly to the graph as string.

enum class Graph {
HOME_GRAPH
}

Now we will create the graph.

NavHost(
navController = navController,
route = Graph.HOME_GRAPH.name,
startDestination = Screen.TEAM.name
) {
composable(route = Screen.TEAM.name) {
TeamsScreen {
navController.navigate("${Screen.TEAM_DETAILS.name}/${it.toJson()}")
}
}
composable(route = Screen.GAMES.name) {
GamesScreen { id ->
navController.navigate("${Screen.GAME_DETAILS.name}/$id")
}
}
}

As you can see the route of navigation component is similar to the web app url. This is what the route looks like in regular string “team/id” . you need include the placeholder for the unique ID of the data.

Finally Type safety is now available in Navigation Compose and Navigation Kotlin DSL!

They are available as of Navigation 2.8.0.

These APIs are equivalent to what Safe Args provides to navigation graphs built-in navigation XML resource files.

Define routes

To use type-safe routes in Compose, you first need to define serializable classes or objects that represent your routes.

  • Object: Use an object for routes without arguments.
  • Class: Use a class or data class for routes with arguments.
  • KClass<T>: Use if you don't need to pass arguments, such as a class without parameters, or a class where all parameters have default values

For example: Exercise::class

In either case, the object or class must be serializable.

@Serializable
object SplashRoute // Define a splash route that doesn't take any arguments

@Serializable
object HomeRoute // Define a home route that doesn't take any arguments

@Serializable
data class Exercise (
val name: String,
val type: String,
val muscle: String,
val equipment: String,
val difficulty: String,
val instructions: String
)

Build your graph

Next is create your navigation graph.

@Composable
fun SetupNavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = SplashRoute,
) {
composable<SplashRoute> {
SplashScreen(navController = navController)
}
composable<HomeRoute> {
HomeScreen(navController = navController)
}
composable<Exercise> { backStackEntry ->
val exercise: Exercise = backStackEntry.toRoute()
ExerciseDetailsScreen(navController, exercise)
}
}
}

Observe the following in this example:

  • composable() takes a type parameter. That is, composable<Exercise>.
  • Defining the destination type is a more robust approach than passing a route string as in composable("exercise").
  • The route class defines the type of each navigation argument, as in val name: String, so there's no need for NavArgument.
  • For the profile route, the toRoute() extension method recreates the Exercise object from the NavBackStackEntry and its arguments.

And last navigate to your type safe route by using the navigate() .

navController.navigate(
Exercise(
name = exercises.value[index].name,
type = exercises.value[index].type,
muscle = exercises.value[index].muscle,
equipment = exercises.value[index].equipment,
difficulty = exercises.value[index].difficulty,
instructions = exercises.value[index].instructions,
),
)

Here’s the final usage!

val exercises = viewModel.exerciseState.collectAsState()
LazyColumn(modifier = Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp)) {
items(exercises.value.size) { index ->
HorizontalDivider(color = Color.Transparent, modifier = Modifier.height(10.dp))
ExerciseCard(exercise = exercises.value[index]) {
navController.navigate(
Exercise(
name = exercises.value[index].name,
type = exercises.value[index].type,
muscle = exercises.value[index].muscle,
equipment = exercises.value[index].equipment,
difficulty = exercises.value[index].difficulty,
instructions = exercises.value[index].instructions,
),
)
}
}
}

You can check the full source code below.

nba-stats — It uses the string-based route

training-camp — It uses the serialized object and classes route

I HOPE THIS WILL HELP YOU GUYS, HAPPY CODING!

--

--