Material 3 Bottom Navigation Bar in Jetpack Compose

Introduction

Bottom navigation is a widely recognized and utilized feature in Android applications, finding its place in virtually every app, from popular platforms like Instagram, Facebook, and Twitter to numerous others. These bottom navigation bars serve as a navigational tool, facilitating seamless movement between key destinations within an app. With the power of Jetpack Compose, creating a dynamic and visually engaging bottom navigation experience has never been more accessible. In this article, we are going to deep dive into the bottom navigation bars in Jetpack Compose.

Setup

Add the dependency in 'app/build.gradle'.

implementation "androidx.navigation:navigation-compose:2.6.0"

Consider adding the latest dependency of the navigation. You can the latest dependency here Navigation Repository.

Step 1. Define Destination

After performing sync in your project, create a separate file in the directory for the bottom navigation destination called 'Destinations'. Now create a sealed class with the name 'Destinations' with the bottom navigation item icon, title, and route, which we will use later for navigation between screens. This approach streamlines the management of navigation destinations while encapsulating relevant information for each destination.

sealed class Destinations(
    val route: String,
    val title: String? = null,
    val icon: ImageVector? = null
) {
    object HomeScreen : Destinations(
        route = "home_screen",
        title = "Home",
        icon = Icons.Outlined.Home
    )

    object Favourite : Destinations(
        route = "favourite_screen",
        title = "Favorite",
        icon = Icons.Outlined.Favorite
    )

    object Notification : Destinations(
        route = "notification_screen",
        title = "Notification",
        icon = Icons.Outlined.Notifications
    )

}

Step 2. Define screens to Display

To facilitate comprehension, I've outlined three screens in a file for illustrative purposes. However, you can easily differentiate between them. Presently, each screen solely features centered text content. 

@Composable
fun HomeScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.DarkGray)
            .wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = "Home Screen",
            style = MaterialTheme.typography.titleLarge,
            color = Color.White,
            modifier = Modifier.align(Alignment.CenterHorizontally),
            textAlign = TextAlign.Center,
        )
    }
}

@Composable
fun FavouriteScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.Magenta)
            .wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = "Favourite Screen",
            style = MaterialTheme.typography.titleLarge,
            color = Color.White,
            modifier = Modifier.align(Alignment.CenterHorizontally),
            textAlign = TextAlign.Center,
        )
    }
}

@Composable
fun NotificationScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.Blue)
            .wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = "Notification Screen",
            style = MaterialTheme.typography.titleLarge,
            color = Color.White,
            modifier = Modifier.align(Alignment.CenterHorizontally),
            textAlign = TextAlign.Center,
        )
    }
}

Step 3. Define Navigation Graph

Here we define a Composable function named NavigationGraph that defines the navigation structure within a Jetpack Compose app. It utilizes a NavHost to manage different destinations facilitated by a NavHostController. The starting destination is set to the route of the "HomeScreen." Within the NavHost, there are three composable blocks, each corresponding to a different screen destination: "HomeScreen," "FavouriteScreen," and "NotificationScreen." As users navigate through the app, the appropriate composable function, such as HomeScreen(), FavouriteScreen(), or NotificationScreen(), will be invoked to render the content of the respective screen. This architecture streamlines navigation and content presentation, ensuring a smooth user experience.

@Composable
fun NavigationGraph(navController: NavHostController) {
    NavHost(navController, startDestination = Destinations.HomeScreen.route) {
        composable(Destinations.HomeScreen.route) {
            HomeScreen()
        }
        composable(Destinations.Favourite.route) {
            FavouriteScreen()
        }
        composable(Destinations.Notification.route) {
            NotificationScreen()
        }
    }
}

Step 4. Create Bottom Navigation with Material 3

The BottomBar Composable function orchestrates the construction of a bottom navigation bar in Jetpack Compose. With inputs including a navigation controller, a mutable state, and an optional modifier for styling, it assembles a set of navigation items corresponding to different screens in the app. Each navigation item incorporates a label and an icon. The currently active route is determined by examining the back stack, enabling proper highlighting of the selected item. When an item is clicked, the navigation controller facilitates seamless screen transitions.

@Composable
fun BottomBar(
    navController: NavHostController, state: MutableState<Boolean>, modifier: Modifier = Modifier
) {
    val screens = listOf(
        Destinations.HomeScreen, Destinations.Favourite, Destinations.Notification
    )

    NavigationBar(
        modifier = modifier,
        containerColor = Color.LightGray,
    ) {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route

        screens.forEach { screen ->

            NavigationBarItem(
                label = {
                    Text(text = screen.title!!)
                },
                icon = {
                    Icon(imageVector = screen.icon!!, contentDescription = "")
                },
                selected = currentRoute == screen.route,
                onClick = {
                    navController.navigate(screen.route) {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                },
                colors = NavigationBarItemDefaults.colors(
                    unselectedTextColor = Color.Gray, selectedTextColor = Color.White
                ),
            )
        }
    }

}

Step 5. Setting up navigation graph and bottom navigation 

In conclusion, we will integrate a bottom bar and navigation graph into the main activity, serving as the pivotal starting point for our application.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BottomBarExampleTheme {

                val navController: NavHostController = rememberNavController()
                val bottomBarHeight = 56.dp
                val bottomBarOffsetHeightPx = remember { mutableStateOf(0f) }

                var buttonsVisible = remember { mutableStateOf(true) }

                Scaffold(
                    bottomBar = {
                        BottomBar(
                            navController = navController,
                            state = buttonsVisible,
                            modifier = Modifier
                        )
                    }) { paddingValues ->
                    Box(
                        modifier = Modifier.padding(paddingValues)
                    ) {
                        NavigationGraph(navController = navController)
                    }
                }
            }
        }
    }
}

Output

Step 6. (Optional). Animating navigation bar based on list scroll

This constitutes an extra feature that can be incorporated into the application, allowing us to animate the navigation bar to automatically close or vanish while scrolling. This functionality is a common sight in numerous popular applications. To integrate this feature, a few adjustments are required within the navigation bar. Initially, encase your navigation bar within the provided AnimatedVisibility function, as shown below.

    AnimatedVisibility(
        visible = state.value,
        enter = slideInVertically(initialOffsetY = { it }),
        exit = slideOutVertically(targetOffsetY = { it }),
    ) {
        NavigationBar(
            modifier = modifier,
            containerColor = Color.LightGray,
        ) {
            //existing code 
        }
    }

To incorporate visibility changes in the bottom bar, just establish the bottom bar's height as 56 dp and set the x offset to 0. For the y offset, employ a function, as our intent is to exclusively animate the height of the bottom bar. Finally, append the function responsible for diminishing the bottom bar's height. With these adjustments in place, you're ready to proceed with testing the application.


            BottomBarExampleTheme {

                val navController: NavHostController = rememberNavController()
                val bottomBarHeight = 56.dp
                val bottomBarOffsetHeightPx = remember { mutableStateOf(0f) }

                var buttonsVisible = remember { mutableStateOf(true) }

                // Listen to the scroll state to update the visibility of buttons
                LaunchedEffect(bottomBarOffsetHeightPx.value) {
                    buttonsVisible.value = bottomBarOffsetHeightPx.value >= -5
                }

                Scaffold(
                    modifier = Modifier.bottomBarAnimatedScroll(
                        height = bottomBarHeight, offsetHeightPx = bottomBarOffsetHeightPx
                    ),
                    bottomBar = {
                        BottomBar(
                            navController = navController,
                            state = buttonsVisible,
                            modifier = Modifier
                                .height(bottomBarHeight)
                                .offset {
                                    IntOffset(
                                        x = 0, y = -bottomBarOffsetHeightPx.value.roundToInt()
                                    )
                                }
                        )
                    }) { paddingValues ->
                     //existing code
                }
            }

fun Modifier.bottomBarAnimatedScroll(
    height: Dp = 56.dp, offsetHeightPx: MutableState<Float>
): Modifier = composed {
    val bottomBarHeightPx = with(LocalDensity.current) {
        height.roundToPx().toFloat()
    }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                val delta = available.y
                val newOffset = offsetHeightPx.value + delta
                offsetHeightPx.value = newOffset.coerceIn(-bottomBarHeightPx, 0f)

                return Offset.Zero
            }
        }
    }

    this.nestedScroll(nestedScrollConnection)
}

Output

Summary

The article guides readers through creating a navigation graph that represents different screens, like Home, Favorites, and Notifications. It highlights how to design and customize each screen using Jetpack Compose's powerful layout components. Moreover, the article introduces an advanced feature – the automatic hiding and appearance of the bottom bar while scrolling. By employing Jetpack Compose's AnimatedVisibility and offset functions, users can achieve an engaging user experience akin to top applications. The article concludes with a comprehensive overview of the entire process, allowing developers to seamlessly integrate a dynamic and aesthetically pleasing bottom navigation bar into their Android apps.

Hope this has been a helpful guide for you; it will be helpful for me if you write your genuine comment about this article. Thank you!


Similar Articles