Type-Safe Bottom Navigation in Jetpack Compose
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
- 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.