Type-Safe Bottom Navigation in Jetpack Compose

G. Raj Kumar
4 min readJul 28, 2024

--

Photo by Road Trip with Raj on Unsplash

With the latest update in Navigation Compose, Jetpack Compose developers have been blessed with a highly anticipated feature: type safety. This enhancement significantly simplifies data passing between screens or destinations, making the development process smoother and less error-prone. Type safety ensures that navigation arguments are strongly typed, reducing runtime errors and enhancing code readability and maintainability. In this blog, we will dive into the implementation of type-safe bottom navigation in Jetpack Compose. We’ll explore setting up navigation destinations, creating a bottom navigation bar, and managing navigation events efficiently. For comprehensive details, refer to the official Android documentation. Let’s get started!

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 Destinations

To start, we need to define our navigation destinations. Each destination represents a screen in our application. We’ll use the @Serializable annotation to mark our destination objects. Here’s how we set it up:

@Serializable
object HomeScreens

@Serializable
object JobsScreens

@Serializable
object SchemesScreens

@Serializable
object ServicesScreens

Sealed Classes for Navigation

Next, we use sealed classes to define our bottom navigation screens. Sealed classes allow us to create a restricted class hierarchy, ensuring that all subclasses are known at compile-time. This is how we set up our destinations with respective icons and names:

@Serializable
sealed class BottomScreens<T>(val name: String, val icon: Int, val route: T) {
@Serializable
data object Home : BottomScreens<HomeScreens>(name = "Home", icon = R.drawable.outline_home, route = HomeScreens)

@Serializable
data object Job : BottomScreens<JobsScreens>(name = "Job", icon = R.drawable.outline_email, route = JobsScreens)

@Serializable
data object Scheme : BottomScreens<SchemesScreens>(name = "Scheme", icon = R.drawable.outline_list, route = SchemesScreens)

@Serializable
data object Service : BottomScreens<ServicesScreens>(name = "Service", icon = R.drawable.outline_build, route = ServicesScreens)
}

In this setup, name is the label displayed in the bottom navigation bar, and icon is the corresponding icon for each destination.

Creating the Bottom Navigation Bar

The main part of our implementation involves creating the bottom navigation bar and handling navigation events. Here’s how to set it up in Jetpack Compose:

@Composable
fun AppBottomNavigation(navController: NavController) {
val bottomScreens = remember {
listOf(
BottomScreens.Home,
BottomScreens.Scheme,
BottomScreens.Service,
BottomScreens.Job
)
}

BottomNavigation(backgroundColor = MaterialTheme.colorScheme.background) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination

bottomScreens.forEach { screen ->
val isSelected = currentDestination?.hierarchy?.any { it.route == screen.route::class.qualifiedName } == true
BottomNavigationItem(
selected = isSelected,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(
imageVector = ImageVector.vectorResource(screen.icon),
contentDescription = screen.name
)
},
label = {
Text(
text = screen.name,
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Gray
)
},
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onBackground
)
}
}
}

This code snippet ensures that the bottom navigation bar is set up with the defined screens, and handles navigation events appropriately. The popUpTo method prevents building a stack of destinations in the back stack for bottom navigation. The saveState parameter saves the state of the destination before popping, while launchSingleTop avoids duplication of destinations in the navigation back stack. Lastly, restoreState helps continue with the saved state.

Integrating Bottom Navigation in the Main Activity

Finally, we integrate our bottom navigation bar into the MainActivity. Here’s how to do it:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
NaxtreDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.White
) {
val navController = rememberNavController()
CentralNavigation(navController = navController)
Box(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding(),
contentAlignment = Alignment.BottomCenter
) {
AppBottomNavigation(navController = navController)
}
}
}
}
}
}

This setup ensures that our bottom navigation bar is available globally in the app, providing a seamless navigation experience.

Conclusion

Thank you for taking the time to read through this tutorial. Implementing type-safe bottom 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!

FAQs

  1. What is type safety in navigation?

Type safety ensures that navigation arguments are strongly typed, reducing runtime errors and enhancing code readability.

2. How do I prevent destination duplication in the back stack?

Use launchSingleTop = true in the navController.navigate() method to avoid duplication.

3. What is the purpose of saveState in navigation?

saveState preserves the state of the destination before it is popped from the back stack.

4. Can I customize the bottom navigation bar appearance?

Yes, you can customize the bottom navigation bar’s appearance by modifying its color scheme, typography, and iconography.

5. How do I test bottom navigation in Jetpack Compose?

You can test bottom navigation by writing unit tests for your navigation components and using Jetpack Compose testing libraries to verify navigation behavior.

--

--