Farewell to String Routes

In a previous article, I discussed how to structure the code for a clearer and more manageable Compose Navigation experience.

This structure was crucial for me in addressing a key challenge I faced with Compose Navigation: managing routes.

Strings can be error-prone for defining routes. Typos and small mistakes are easy to miss, making debugging difficult as the editor doesn’t flag them as errors.

Even with this structure, managing parameters still requires some string usage. While it minimizes the issue, it doesn’t entirely eliminate it.

Finally with the new version of Compose Navigation, the 2.8.0, we can eliminates the error-prone nature of string-based routes and makes navigation development more robust.

Before we dive in, a quick tip! If you haven’t already, consider reading my previous article, It lays the foundation of the same navigation structure we’ll be speaking here.

Let’s code

It’s important to note that at the time of writing, Compose Navigation 2.8.0 is in alpha stage (version alpha08). This means the final API might change slightly before the official release.

Like always, we need to start with the dependencies, we need to add Kotlin Serialization and, obviusly, the new version for the navigation library:

[versions]
kotlin = "1.9.23"
navigationCompose = "2.8.0-alpha08"
kotlinxSerializationJson = "1.6.3"
...
[libraries]

androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
...

[plugins]

jetbrainsKotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
...

Ok, with the library updated we can start on migrating our routes. Compose Navigation 2.8.0 introduces a powerful concept: defining screens as data class and data object. This allows for type-safety and better code organization. In my case I prefer use a sealed interface that elegantly represents all possible screens within our app, promoting clarity and preventing unexpected navigation states.

sealed interface Screen {
@Serializable
data object Home : Screen

@Serializable
data object Topics : Screen

...

@Serializable
data class Zoom(val id: String) : Screen

}

As you can see, when the screen need values passed we can simply define a data class and add the values as parameters.

Now, let’s shift our focus to the NavigationHost. Here, you’ll witness a significant improvement in how screens are defined.

We can migrate our code from:

NavHost(
navController = navigationViewModel.activityNavController,
startDestination = ScreenEnum.Home.name,
modifier = modifier
) {

composable(ScreenEnum.Home.name) {
HomeInitScreen(onNavigationEvent = navigationViewModel::onEvent)
}

composable(
route = "${ScreenEnum.Zoom.name}/{id}",
arguments = listOf(
navArgument("id") {
type = NavType.StringType
},
),
) {

val zoomId by remember {
mutableStateOf(it.arguments?.getString("id"))
}

ZoomInitScreen(
zoomId = zoomId!!,
onNavigationEvent = navigationViewModel::onEvent)
}
}

to:

NavHost(
navController = navigationViewModel.activityNavController,
startDestination = Screen.Home,
modifier = modifier
) {

composable<Screen.Home> {
HomeInitScreen(onNavigationEvent = navigationViewModel::onEvent)
}

composable<Screen.Zoom> { backStackEntry ->
val id = backStackEntry.toRoute<Screen.Zoom>().id
ZoomInitScreen(
id = id,
onNavigationEvent = navigationViewModel::onEvent
)
}
}

The code transformation within the NavigationHost becomes particularly evident when dealing with parameters. Gone are the days of extracting arguments from string names. Now, you can directly access all parameters defined within the corresponding screen’s data class.

Also the navigation is simplified we passed from:

activityNavController.navigate("${ScreenEnum.Zoom.name}/$id")

to:

activityNavController.navigate(Screen.Zoom(id))

Functions now accept your screens directly as parameters. This enhances flexibility, particularly if you have custom implementations that interact with navigation functions.

Source link