Bottom navigation in jetpack compose using Material3.

Bharadwaj Rns
9 min readAug 11, 2023

--

Bottom navigation is one of the most important things to know in your application development journey, Most applications use the bottom navigation to navigate from one screen to another screen. And also to share the data between the screens, This bottom navigation play’s a significant role in balancing the user interface. And when it comes to Android luckily Android has its own bottom navigation component that can support Material 2 and 3 library. And in this article, we will learn about implementing bottom navigation using the jetpack compose and material 3.

✨ What we are going to build in this article🤔?

In this session, we are going to build a bottomNavigationBar using jetpack compose and material 3. And also I am going to show you the efficient way of implementing the bottom bar in an efficient way so, you can reuse where ever your client wants 😒. And the final product is going to look like this.

✨Let’s get started❤️‍🔥!

Create an empty compose project in android studio or IntelliJ, depending on your mood🥱. Now after building the project, the initial project is going to look like this.

Before implementing the bottom navigation bar, Let’s add this tiny dependency to our build.gradle(app)

//dependency for the navigation.  
implementation "androidx.navigation:navigation-compose:2.7.0-rc01"

For implementing the bottom navigation we basically need three necessary components they are

  • Navigation item label
  • Navigation item icon
  • Navigation route

What is this Navigation item? basically, navigation items are nothing but icons and labels that are visible inside the bottom navigation row.

And what is the navigation route? usually, a route means a direction or the path to the destination. As in this nav Host also follows the route or direction of the specific destination or the screen. In the context of Android development, “routes” typically refer to the different screens or destinations within your app that users can navigate to. Each route corresponds to a different UI state or screen. The Navigation Component provides a way to define and manage these routes using a graph-based navigation structure.

✨ Creating Navigation routes

Now let’s create our routes for the bottom navigation. By using the sealed class.

A sealed class is a special type of class in Kotlin that is used to represent a restricted hierarchy of classes, where each subclass is known and predefined.

sealed class Screens(val route : String) {
object Home : Screens("home_route")
object Search : Screens("search_route")
object Profile : Screens("profile_route")
}

These are not the original composable screen, they are just routes that represent different screens.

We have three screens for our bottom navigation, By using the sealed class.

✨ Creating Navigation Items

Now we have our screens with their respective roots, Let’s create the bottom navigation items for our bottom navigation bar by using thedata class.

A data class is a special type of class that is primarily used to hold data, representing simple, immutable data structures.

Now let’s create the navigation items by using the data class.

//initializing the data class with default parameters
data class BottomNavigationItem(
val label : String = "",
val icon : ImageVector = Icons.Filled.Home,
val route : String = ""
) {

//function to get the list of bottomNavigationItems
fun bottomNavigationItems() : List<BottomNavigationItem> {
return listOf(
BottomNavigationItem(
label = "Home",
icon = Icons.Filled.Home,
route = Screens.Home.route
),
BottomNavigationItem(
label = "Search",
icon = Icons.Filled.Search,
route = Screens.Search.route
),
BottomNavigationItem(
label = "Profile",
icon = Icons.Filled.AccountCircle,
route = Screens.Profile.route
),
)
}
}

✨ Setting up the Bottom Navigation Bar

Here comes the important part now let’s set up the bottom navigation bar by using Scaffold.

A Scaffold is a composable function that provides a basic structure for building a consistent and standardized app layout.

Now we are going to set up our bottom Navigation Bar in a kotlin file. Let’s create a kotlin file called BottomNavigationBar()

Inside the BottomNavigationBar() we are going to create our scaffold that contains our bottomNavigationBar.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomNavigationBar() {
//initializing the default selected item
var navigationSelectedItem by remember {
mutableStateOf(0)
}
/**
* by using the rememberNavController()
* we can get the instance of the navController
*/
val navController = rememberNavController()

//scaffold to hold our bottom navigation Bar
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
NavigationBar {
//getting the list of bottom navigation items for our data class
BottomNavigationItem().bottomNavigationItems().forEachIndexed {index,navigationItem ->

//iterating all items with their respective indexes
NavigationBarItem(
selected = index == navigationSelectedItem,
label = {
Text(navigationItem.label)
},
icon = {
Icon(
navigationItem.icon,
contentDescription = navigationItem.label
)
},
onClick = {
navigationSelectedItem = index
navController.navigate(navigationItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { paddingValues ->
//We need to setup our NavHost in here
}
}

I know the above snippet is a quite mess. Let me explain the above functionality of the above code. In our BottomNavigationBar() function first we have the navController. What the hell is that navController?

The NavController is used to manage navigation within your Compose-based Android app. It facilitates the navigation between different composables/screens, backstack management, and passing data between screens.

to get the instance of the navController we use rememberNavController() in compose.

Now let’s jump into our NavigationBarItem() function, This function is useful to set the bottom navigation item to the bottom navigation bar. Let’s see the parameters of this function.

@Composable
fun RowScope.BottomNavigationItem(
selected: Boolean,
onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
selectedContentColor: Color = LocalContentColor.current,
unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
)

Now back to our code,

    NavigationBarItem(
/*If our current index of the list of items
*is equal to navigationSelectedItem then simply
*The selected item is active in overView this
*is used to know the selected item
*/
selected = index == navigationSelectedItem,

//Label is used to bottom navigation labels like Home, Search
label = {
Text(navigationItem.label)
},

// Icon is used to display the icons of the bottom Navigation Bar
icon = {
Icon(
navigationItem.icon,
contentDescription = navigationItem.labe)
},
// used to handle click events of navigation items
onClick = {
navigationSelectedItem = index
navController.navigate(navigationItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)

Yay❤️‍🔥😍! You successfully finished setting up your bottom navigation bar now the crispiest part is to give life to our bottom navigation bar to work properly we can achieve that by using the navHost.

Setting up the NavHost

This navHost plays a major role in maintaining our routes. Question? how? To answer this question first we need to know what is this navHost actually.

The NavHost composable is a key component in creating a navigational structure for your app. It is used to display different composables based on the current navigation destination, which is managed by the NavController.

Let’s try to understand this in a simple way. Firstly we created one navigation graph that contains all our screens with their respective routes. And now we need someone to handle our navigation graph. In this situation, the navHost will take responsibility. Now we set up a NavHost and connected it to a navigation graph. The NavController manages the navigation between different destinations as defined in the graph, and the NavHost displays the appropriate composables based on the current destination.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomNavigationBar() {
var navigationSelectedItem by remember {
mutableStateOf(0)
}
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
NavigationBar {
BottomNavigationItem().bottomNavigationItems().forEachIndexed {index,navigationItem ->
NavigationBarItem(
selected = index == navigationSelectedItem,
label = {
Text(navigationItem.label)
},
icon = {
Icon(
navigationItem.icon,
contentDescription = navigationItem.label
)
},
onClick = {
navigationSelectedItem = index
navController.navigate(navigationItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) {paddingValues ->
NavHost(
navController = navController,
startDestination = Screens.Home.route,
modifier = Modifier.padding(paddingValues = paddingValues)) {
composable(Screens.Home.route) {
//call our composable screens here
}
composable(Screens.Search.route) {
//call our composable screens here
}
composable(Screens.Profile.route) {
//call our composable screens here
}
}
}
}

Now inside the composable block, we need to call the actual composable screens that we haven’t created yet. And the startDestination is used to tell our navHost that we are going to start the navigation graph from here. Now let’s create our actual composable content screens.

Setting up the Screens

Let’s create a package called screens. That contains all our composable screens.

  1. Home Screen
@Composable
fun HomeScreen(navController: NavController) {
NavigationBarMediumTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.fillMaxSize().padding(15.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier.fillMaxWidth()
.height(200.dp)
.padding(horizontal = 15.dp, vertical = 10.dp)
.clip(MaterialTheme.shapes.large)
) {
Image(
painter = painterResource(R.drawable.images),
contentDescription = "home_screen_bg",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
Text(
"Home Screen",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 20.dp)
)
}
}
}
}

2. Search Screen

@Composable
fun SearchScreen(navController: NavController) {
NavigationBarMediumTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.fillMaxSize().padding(15.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier.fillMaxWidth()
.height(250.dp)
.padding(horizontal = 15.dp, vertical = 10.dp)
.clip(MaterialTheme.shapes.large)
) {
Image(
painter = painterResource(R.drawable.two),
contentDescription = "search_screen_bg",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
Text(
"Search Screen",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 20.dp)
)
}
}
}
}

3. Profile Screen

@Composable
fun ProfileScreen(navController: NavController) {
NavigationBarMediumTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.fillMaxSize().padding(15.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier.fillMaxWidth()
.height(200.dp)
.padding(horizontal = 15.dp, vertical = 10.dp)
.clip(MaterialTheme.shapes.large)
) {
Image(
painter = painterResource(R.drawable.three),
contentDescription = "profile_screen_bg",
contentScale = ContentScale.Crop
)
}
Text(
"Profile Screen",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 20.dp)
)
}
}
}
}

And the final project structure is going to look like this.

Final project structure.

Now add these screens to our navHost to perform the navigation.

@Composable
fun BottomNavigationBar() {
var navigationSelectedItem by remember {
mutableStateOf(0)
}
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
NavigationBar {
BottomNavigationItem().bottomNavigationItems().forEachIndexed {index,navigationItem ->
NavigationBarItem(
selected = index == navigationSelectedItem,
label = {
Text(navigationItem.label)
},
icon = {
Icon(
navigationItem.icon,
contentDescription = navigationItem.label
)
},
onClick = {
navigationSelectedItem = index
navController.navigate(navigationItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) {paddingValues ->
NavHost(
navController = navController,
startDestination = Screens.Home.route,
modifier = Modifier.padding(paddingValues = paddingValues)) {
composable(Screens.Home.route) {
HomeScreen(
navController
)
}
composable(Screens.Search.route) {
SearchScreen(
navController
)
}
composable(Screens.Profile.route) {
ProfileScreen(
navController
)
}
}
}
}

And the final BottomNavigationBar() file is going to look like this.

✨ Result

Now after finishing our application, it’s time to test our bottom navigation bar. After installing the application on our device this is how to application is going to look like.

Give yourself a treat😍😊. We successfully build our bottom navigation bar by using jetpack compose and in the later sessions we are going to learn about implementing nested graphs in jetpack compose. Thanks for reading this article. Let’s learn implement and evolve together, Have a nice day.

Here is the GitHub repo link: https://github.com/BharadwajRns/BottomNavigationCompose.git

--

--