You have your home screen with a bottom navigation bar that includes different destinations. But what if each destination also needs to navigate to a nested destination? In such cases, the bottom navigation bar would still be visible, which might not be the desired behavior. So, what’s the workaround?
You can add a condition to hide the bottom navigation bar if the current destination is not part of the bottom navigation.
The problem with using such a condition is the timing of when the bottom navigation hides itself. You might notice how the new content adjusts itself as the bottom navigation enters the hiding state. To address this, you could add another workaround: animating the bottom navigation's visibility. But with all this complexity, is there a cleaner way to achieve a natural, seamless look when navigating to different destinations—without worrying about whether the current destination is part of the bottom navigation?
The answer is yes! This time, I’ll guide you through a cleaner approach: using a nested NavHost.
Destination
Let’s create an interface called HomeDestination.
interface HomeDestination {
val label: String
val icon: ImageVector
val contentDescription: String
val route: KClass<*>
}
We implement this HomeDestination for each different destination. In this case, we’ll use an enum class to define them.
enum class TopLevelDestination(
override val label: String,
override val icon: ImageVector,
override val contentDescription: String,
override val route: KClass<*>,
) : HomeDestination {
APPS(
label = "Apps",
icon = Icons.Apps,
contentDescription = "Apps",
route = AppsRouteData::class,
),
SERVICE(
label = "Service",
icon = Icons.RemoveRedEye,
contentDescription = "Service",
route = ServiceRouteData::class,
),
SETTINGS(
label = "Settings",
icon = Icons.Settings,
contentDescription = "Service",
route = SettingsRouteData::class,
),
}
Home Screen
This will hold our Bottom Navigation. I’m using NavigationSuiteScaffold to create a responsive navigation layout. The code below also includes a SnackBarHostState and a TopBar, which are only available on this Home Screen.
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
snackbarHostState: SnackbarHostState,
navController: NavHostController = rememberNavController(),
topLevelDestinations: List<HomeDestination>,
startDestination: KClass<*>,
onDestinationClick: (NavHostController, HomeDestination) -> Unit,
builder: NavGraphBuilder.() -> Unit,
) {
val topAppBarScrollBehavior = enterAlwaysScrollBehavior()
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
val topBarTitleStringResource = topLevelDestinations.find { destination ->
currentDestination.isTopLevelDestinationInHierarchy(destination.route)
}?.label ?: topLevelDestinations.first().label
NavigationSuiteScaffold(
navigationSuiteItems = {
topLevelDestinations.forEach { destination ->
item(
icon = {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(id = destination.contentDescription),
)
},
label = { Text(stringResource(id = destination.label)) },
selected = currentDestination.isTopLevelDestinationInHierarchy(destination.route),
onClick = {
onDestinationClick(navController, destination)
},
)
}
},
) {
Scaffold(
topBar = {
LargeTopAppBar(
title = {
Text(
text = stringResource(id = topBarTitleStringResource),
)
},
scrollBehavior = topAppBarScrollBehavior,
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
NavHost(
modifier = modifier
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection)
.padding(paddingValues)
.consumeWindowInsets(paddingValues),
navController = navController,
startDestination = startDestination,
builder = builder,
)
}
}
}
private fun NavDestination?.isTopLevelDestinationInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false
NavHost
We place the HomeScreen composable inside our NavHost. To make this cleaner, it’s better to create an extension function for NavGraphBuilder.
fun NavGraphBuilder.homeScreen(
snackbarHostState: SnackbarHostState,
topLevelDestinations: List<HomeDestination>,
startDestination: KClass<*>,
onDestinationClick: (NavHostController, HomeDestination) -> Unit,
builder: NavGraphBuilder.() -> Unit,
) {
composable<HomeRouteData> {
HomeScreen(
snackbarHostState = snackbarHostState,
topLevelDestinations = topLevelDestinations,
startDestination = startDestination,
onDestinationClick = onDestinationClick,
builder = builder,
)
}
}
In our main NavHost, we can now call the homeScreen extension function to include the HomeScreen composable.
@Composable
fun MyNavHost(
navController: NavHostController,
) {
val snackbarHostState = remember {
SnackbarHostState()
}
NavHost(
navController = navController,
startDestination = HomeRouteData::class,
) {
homeScreen(
snackbarHostState = snackbarHostState,
topLevelDestinations = TopLevelDestination.entries,
startDestination = AppsRouteData::class,
onDestinationClick = { homeNavHostController, homeDestination ->
when (homeDestination) {
APPS -> homeNavHostController.navigateToApps()
SERVICE -> homeNavHostController.navigateToService()
SETTINGS -> homeNavHostController.navigateToSettings()
}
},
builder = {
appsScreen()
serviceScreen()
settingsScreen()
},
)
appDetailsScreen()
}
As you can see in onDestinationClick, it uses its own NavController called homeNavHostController. This NavController is tied to the BottomNavigation and is responsible solely for navigating between the different bottom navigation destinations. The builder inside it contains all our home destinations.
Finally, our MainActivity should look like this,
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
val navController = rememberNavController()
MyNavHost(navController = navController)
}
}
}
By using a nested NavHost and separating navigation responsibilities, you can create a clean and intuitive navigation flow in Jetpack Compose. This approach ensures your bottom navigation and top bar appear only where needed, while nested destinations remain focused.