Bottom Navigation Bar in Jetpack Compose

Sioso
4 min readJul 31, 2023

The Bottom Navigation Bar is a powerful tool for app development, as it provides an intuitive, easy way for a user to quickly navigate between key destinations in an app!

Navigation in Compose:

In Jetpack Compose, navigation is handled through the NavHost and the NavController:

🎮 NavController: keeps track of the current destination and the backstack (which helps you automatically implement the back button in Android, shown below)

🏠 NavHost: associated with a NavController -> describes the destinations in the app using “routes”. These should be the composables that the user can navigate between! Or in our case, the three screens specified in the navigation bar.

Okay, let’s get started!

Destination Screens

I’ve set up the three dummy screens HomeScreen, TasksScreen, and OptionsScreenlike so:

@Composable
fun HomeScreen(paddingModifier: Modifier) {
Box(
modifier = paddingModifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.BottomCenter,
) {
Text(text="home", fontSize = 100.sp)
}
}

BottomBarScreen

Now we can put the information for each navigation screen into a sealed class. Each destination has a route, which is how the NavController will navigate to that screen. Additionally, the label and icon which will be displayed on the navigation bar are in this class.

sealed class BottomBarScreen(
val route: String,
val label: String,
val icon:ImageVector
) {
object Home: BottomBarScreen(
route="home",
label="Home",
icon= Icons.Rounded.Home
)
object Tasks: BottomBarScreen(
route="tasks",
label="Tasks",
icon= Icons.Rounded.CheckCircle
)
object Options: BottomBarScreen(
route="options",
label="Options",
icon= Icons.Rounded.Settings
)
}

Navigation Graph

Okay, we can now create the Navigation Graph, which will contain our NavHost. This is basically letting the NavHost know what the destinations are, through the screen routes we just specified. (Note that we are passing in a NavController, which is what will actually navigate between these destinations in the NavHost)

@Composable
fun BottomNavigationGraph(
navController: NavHostController,
paddingModifier: Modifier
) {
NavHost(navController = navController,
startDestination = BottomBarScreen.Home.route
) {
composable(route= BottomBarScreen.Home.route) {
HomeScreen(paddingModifier)
}
composable(route= BottomBarScreen.Tasks.route) {
TasksScreen(paddingModifier)
}
composable(route= BottomBarScreen.Settings.route) {
MenuScreen(paddingModifier)
}
}
}

MainActivity Setup

Great! Most of the work is complete for the navigation. Let’s go to MainActivity.kt and call MainScreen() , which is where the navController is declared.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme{
MainScreen()
}
}
}
}
@Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = { AppBottomBar(navController = navController) },
) //content:
{paddingValues->
BottomNavigationGraph(
navController = navController,
paddingModifier = Modifier.padding(paddingValues)
)
}
}

Scaffold is a component (androidx.compose.material3.Scaffold) that features a bottomBar, topBar, floatingActionButton, snackbar, etc. This makes it perfect to easily implement the bottom navigation bar.

App Bottom Bar

In the Scaffold , we set the bottomBar to the Composable AppBottomBar , which is implemented here:

@Composable
fun AppBottomBar(navController: NavHostController) {
val screens = listOf(
BottomBarScreen.Home,
BottomBarScreen.Tasks,
BottomBarScreen.Settings
)
BottomNavigation() {
screens.forEach { screen ->
AddItem(
screen = screen,
navController = navController
)
}
}
}

Note! You must use androidx.compose.material.BottomNavigation and androidx.compose.material.BottomNavigationItem . Make sure you are not using material3 for this step, or it will not work!!

We are using the composable BottomNavigation, provided in Compose. To define the content in the bar, we use a custom function AddItem , as shown here:

@Composable
fun RowScope.AddItem(
screen: BottomBarScreen,
navController: NavHostController
) {
val backStackEntry = navController.currentBackStackEntryAsState()
BottomNavigationItem(
label = {
Text(text = screen.label)
},
icon = {
Icon(
imageVector = screen.icon,
contentDescription = screen.route + " icon"
)
},
selected = screen.route == backStackEntry.value?.destination?.route,
onClick = {
navController.navigate(screen.route)
}
)
}
  • note that we are using the navHostController to see the backstack, which allows us to see which destination we are currently on -> set the selected property

Putting it all together!

Now we have a working navigation bar that navigates to the 3 screens, and the content in the screen is placed nicely inside the screen, above the navigation bar.

Other Details

One more thing: usually when we are navigating between tabs, the back press on Android shouldn’t switch between tabs. Instead, it should take us back to the Home screen! If we exit the Home screen again, it should exit the application.

To fix this, modify the onClick in the AddItem function:

onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id)
launchSingleTop = true
}
}

This brings us to the startDestination (which we set to the home screen in the navigation graph). Also, launchSingleTop ensures that our backstack will not contain multiple copies of the same tab on the stack. Nice!

Done!!

With that, we have completed the entire navigation bar 🥳

For more information on navigation in Jetpack Compose, see here: https://developer.android.com/jetpack/compose/navigation

--

--