DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Bottom Navigation with Badges in Compose — Complete NavigationBar Guide

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) }
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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) }
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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() }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Master the bottom navigation pattern and build intuitive, polished navigation experiences in Compose!


Get 8 Android app templates: Gumroad

Top comments (0)