Explore how to build a complete bottom navigation system in Jetpack Compose with badges, animations, and scroll-aware visibility.
NavigationBar + NavigationBarItem Basics
@Composable
fun BottomNavigation(
selectedTab: Int,
onTabSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
NavigationBar(modifier = modifier) {
listOf("Home", "Search", "Favorites", "Profile").forEachIndexed { index, label ->
NavigationBarItem(
icon = { Icon(Icons.Default.Home, contentDescription = label) },
label = { Text(label) },
selected = selectedTab == index,
onClick = { onTabSelected(index) }
)
}
}
}
BadgedBox for Notification Counts
@Composable
fun BottomNavWithBadges(
selectedTab: Int,
onTabSelected: (Int) -> Unit,
badgeCounts: Map<Int, Int> = emptyMap()
) {
NavigationBar {
listOf("Home", "Search", "Favorites", "Profile").forEachIndexed { index, label ->
NavigationBarItem(
icon = {
BadgedBox(
badge = {
if ((badgeCounts[index] ?: 0) > 0) {
Badge { Text(badgeCounts[index].toString()) }
}
}
) {
Icon(Icons.Default.Home, contentDescription = label)
}
},
label = { Text(label) },
selected = selectedTab == index,
onClick = { onTabSelected(index) }
)
}
}
}
Scroll-Aware Visibility with AnimatedVisibility
@Composable
fun ScrollAwareBottomNav(
selectedTab: Int,
onTabSelected: (Int) -> Unit,
lazyListState: LazyListState = rememberLazyListState()
) {
var isVisible by remember { mutableStateOf(true) }
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.isScrollingUp() }
.collect { isScrollingUp ->
isVisible = isScrollingUp
}
}
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically { it },
exit = slideOutVertically { it }
) {
NavigationBar {
// NavigationBarItem entries...
}
}
}
// Helper extension
fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by mutableStateOf(0)
var previousScrollOffset by mutableStateOf(0)
return remember {
derivedStateOf {
val currentIndex = firstVisibleItemIndex
val currentOffset = firstVisibleItemScrollOffset
val isScrollingUp =
currentIndex < previousIndex ||
(currentIndex == previousIndex && currentOffset < previousScrollOffset)
previousIndex = currentIndex
previousScrollOffset = currentOffset
isScrollingUp
}
}.value
}
Preserving Tab State with rememberSaveable
@Composable
fun MainScreen() {
var selectedTab by rememberSaveable { mutableStateOf(0) }
Scaffold(
bottomBar = {
NavigationBar {
repeat(4) { index ->
NavigationBarItem(
icon = { /* icon */ },
label = { Text("Tab ${index + 1}") },
selected = selectedTab == index,
onClick = { selectedTab = index }
)
}
}
}
) { padding ->
when (selectedTab) {
0 -> HomeScreen(Modifier.padding(padding))
1 -> SearchScreen(Modifier.padding(padding))
2 -> FavoritesScreen(Modifier.padding(padding))
else -> ProfileScreen(Modifier.padding(padding))
}
}
}
Complete Example with NavHost
@Composable
fun MainApp() {
val navController = rememberNavController()
var selectedTab by rememberSaveable { mutableStateOf("home") }
Scaffold(
bottomBar = {
NavigationBar {
listOf("home" to "Home", "search" to "Search", "favorites" to "Favorites", "profile" to "Profile")
.forEach { (route, label) ->
NavigationBarItem(
icon = { Icon(Icons.Default.Home, contentDescription = label) },
label = { Text(label) },
selected = selectedTab == route,
onClick = {
selectedTab = route
navController.navigate(route) {
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { padding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(padding)
) {
composable("home") { HomeScreen() }
composable("search") { SearchScreen() }
composable("favorites") { FavoritesScreen() }
composable("profile") { ProfileScreen() }
}
}
}
Master the bottom navigation pattern and build intuitive, polished navigation experiences in Compose!
Get 8 Android app templates: Gumroad
Top comments (0)