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!