Type-Safe Nested Navigation in Jetpack Compose
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!