Type-Safe Nested Navigation in Jetpack Compose

G. Raj Kumar
3 min readJul 29, 2024

--

Photo by Mulyadi on Unsplash

Navigating through different screens in an app efficiently and safely is crucial for providing a seamless user experience. The latest update in Navigation Compose introduces type safety, a feature that significantly simplifies the process of passing data between screens or destinations. This enhancement reduces errors and improves code readability and maintainability by leveraging Kotlin’s strong typing system. In this blog, we will explore how to implement type-safe nested navigation in Jetpack Compose. We’ll guide you through creating navigation graphs, defining routes with type safety, handling navigation events, and integrating these with your app’s architecture. Let’s dive in!

First let’s import the dependencies we need for type safe navigation

Configure your app level gradle file

plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrains.kotlin.serialization)
id("kotlin-parcelize") // needed only for non-primitive classes
}

dependencies {
//navigation
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
}

Configure your lib.versions.toml file

[versions]
kotlinxSerializationJson = "1.6.3"
kotlinxSerialization = "1.9.23"
navigationCompose = "2.8.0-beta05"

[libraries]
#navigation
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }

[plugins]
jetbrains-kotlin-serialization = { id ="org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerialization"}

Configure your project level gradle file

plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.jetbrains.kotlin.serialization) apply false
}

Setting Up Navigation Graphs

Let’s start by creating our navigation graphs. In this example, we will create a root navigation graph and integrate multiple nested graphs into it. Here’s how to set up the root navigation graph:

@Composable  
fun CentralNavigation(navController: NavHostController) {
NavHost(navController = navController, startDestination = HomeScreens) {
homeNavGraph(navController)
jobsNavGraph(navController)
serviceNavGraph(navController)
schemeNavGraph(navController)
}
}

In this setup, we pass the navController to each nested graph, allowing it to handle navigation independently.

Defining Type-Safe Routes

To define routes with type safety, we can use objects and data classes instead of strings. This approach eliminates typos and enhances code readability. Here’s an example:

@Serializable  
object JobsScreens

@Serializable
object JobRoot

@Serializable
data class JobCategory(val categoryName: String)

@Serializable
object JobEligibility

With these definitions, we can now set up our nested navigation graph.

Creating Nested Navigation Graphs

We will create a navigation graph for job-related screens. This example demonstrates how to use type-safe routes:

fun NavGraphBuilder.jobsNavGraph(navController: NavController) {  
navigation<JobsScreens>(startDestination = JobRoot) {
composable<JobRoot> {
JobRootScreen {
handleJobNavigation(it, navController)
}
}
composable<JobCategory> {
val args = it.toRoute<JobCategory>()
CategoryDetailScreen(categoryName = args.categoryName) {
handleJobNavigation(it, navController)
}
}
composable<JobEligibility> {
JobEligibilityForm()
}
}
}

Handling Navigation Events

To handle navigation events, we create a helper function that processes different navigation actions. Here’s an example:

fun handleJobNavigation(event: JobNavigationEvent, navController: NavController) {  
when(event) {
is JobNavigationEvent.OnCategoryClick -> navController.navigate(JobCategory(categoryName = event.categoryName))

JobNavigationEvent.OnBackPressed -> navController.popBackStack()

JobNavigationEvent.OnApplyNowClick -> navController.navigate(JobEligibility)
}
}

Implementing Navigation Helper Functions

Creating a sealed class to list all navigation events ensures a clear and type-safe way to manage navigation:

sealed class JobNavigationEvent {  
data object OnBackPressed : JobNavigationEvent()
data class OnCategoryClick(val categoryName: String) : JobNavigationEvent()
data object OnApplyNowClick : JobNavigationEvent()
}

Integrating with Composables

We can handle navigation events within composables by exposing lambda functions. Here’s an example of a composable screen:

@Composable  
fun JobRootScreen(handleNavigation: (JobNavigationEvent) -> Unit) {
Scaffold(topBar = { SecondaryHeader() }) {
Column(
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
Button(onClick = { handleNavigation(JobNavigationEvent.OnCategoryClick("Marketing Job"))})
}
}
}

Fetching and Using Passed Data

On the CategoryDetailScreen, we can fetch the passed data and perform operations like making API calls:

@Composable  
fun CategoryDetailScreen(
categoryName: String,
viewModel: CategoryDetailViewModel = hiltViewModel(),
handleNavigation: (JobNavigationEvent) -> Unit
) {
val state by viewModel.state.collectAsState()

LaunchedEffect(Unit) {
viewModel.getCategoryDetails(categoryName)
}
}

Integrating Nested Navigation with Bottom Navigation

To integrate nested navigation with a bottom navigation bar, refer to the detailed blog here.

Conclusion

Thank you for taking the time to read through this tutorial. Implementing type-safe nested navigation in Jetpack Compose not only enhances your app’s reliability but also improves the overall development experience. If you have any questions or suggestions, feel free to reach out. Happy coding!

--

--