Skip to main content
The CptNavigationBar is a Material3-based bottom navigation component that provides primary navigation between the app’s main sections. It integrates with the navigation system and maintains visual selection state.

Overview

Package: es.mobiledev.commonandroid.ui.component.navigationBar File: commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/component/navigationBar/CptNavigationBar.kt:12 A composable function that renders a bottom navigation bar with selectable items corresponding to app modules.

API Reference

CptNavigationBar

@Composable
fun CptNavigationBar(
    selectedModule: NavigationModule,
    modifier: Modifier = Modifier,
    onClickModule: (AppScreens) -> Unit,
)

Parameters

selectedModule
NavigationModule
required
The currently selected navigation module, used to highlight the active tab
modifier
Modifier
default:"Modifier"
Modifier to be applied to the navigation bar container
onClickModule
(AppScreens) -> Unit
required
Callback invoked when a navigation item is clicked, receives the destination screen

Implementation Details

Visual Design

Container Color
Color
Uses MaterialTheme.colorScheme.primary as the background color
Icon States
Color Scheme
  • Selected: MaterialTheme.colorScheme.onPrimary
  • Unselected: MaterialTheme.colorScheme.outlineVariant
The navigation bar displays items for the following screens:
val screens = listOf(
    AppScreens.Home,
    AppScreens.Test,
)
Only modules with hasOwnTab = true in the NavigationModule enum are displayed in the navigation bar.

Usage Examples

Basic Usage

@Composable
fun MyApp() {
    val navController = rememberNavController()
    var currentModule by remember { mutableStateOf(NavigationModule.HOME) }

    Scaffold(
        bottomBar = {
            CptNavigationBar(
                selectedModule = currentModule,
                onClickModule = { screen ->
                    navController.navigate(screen)
                    currentModule = screen.module
                }
            )
        }
    ) { paddingValues ->
        // App content
    }
}

With Navigation Controller

@Composable
fun AppNavigation(navController: NavHostController) {
    val currentScreen by navController.currentBackStackEntryAsState()
    val currentModule = currentScreen?.destination?.route?.let { route ->
        // Derive module from route
    } ?: NavigationModule.HOME

    ScreenWrapper(
        bottomBar = {
            CptNavigationBar(
                selectedModule = currentModule,
                modifier = Modifier,
                onClickModule = { screen ->
                    navController.navigate(screen) {
                        popUpTo(screen) { inclusive = true }
                    }
                },
            )
        },
        showBottomBar = true
    ) { paddingValues ->
        NavHost(navController, startDestination = AppScreens.Home) {
            // Navigation graph
        }
    }
}

Conditional Display

@Composable
fun ConditionalNavBar(showNavBar: Boolean) {
    if (showNavBar) {
        CptNavigationBar(
            selectedModule = NavigationModule.HOME,
            onClickModule = { screen ->
                // Handle navigation
            }
        )
    }
}

CptNavigationBarItem

File: commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/component/navigationBar/CptNavigationBarItem.kt:18 Internal composable for individual navigation items.
@Composable
fun RowScope.CptNavigationBarItem(
    @DrawableRes iconRes: Int,
    isSelected: Boolean,
    onClickModule: () -> Unit,
)

Parameters

iconRes
@DrawableRes Int
required
Drawable resource ID for the item’s icon
isSelected
Boolean
required
Whether this item is currently selected
onClickModule
() -> Unit
required
Callback invoked when this item is clicked

Item Styling

Height
Dimension
Uses R.dimen.navigation_bar__item_height from resources
Indicator Color
Color
Transparent (no background indicator)
Icon Colors
NavigationBarItemDefaults.colors()
NavigationBarItemDefaults.colors(
    selectedIconColor = MaterialTheme.colorScheme.onPrimary,
    unselectedIconColor = MaterialTheme.colorScheme.outlineVariant,
    indicatorColor = Color.Transparent,
)

Integration with Navigation

enum class NavigationModule(val hasOwnTab: Boolean = false) {
    LAUNCHER,
    HOME(hasOwnTab = true),
    TEST(hasOwnTab = true),
    ARTICLE_DETAIL
}
Only modules with hasOwnTab = true appear in the navigation bar. Detail screens like ARTICLE_DETAIL and initialization screens like LAUNCHER are excluded.

AppScreens Integration

@Serializable
sealed interface AppScreens {
    val module: NavigationModule
    val hasTopBar: Boolean
    val hasBottomBar: Boolean

    @Serializable
    data object Home : AppScreens {
        override val module = NavigationModule.HOME
        override val hasBottomBar = true
    }
}

Behavioral Features

Click Handling

val isSelected = selectedModule == screen.module
CptNavigationBarItem(
    isSelected = isSelected,
    onClickModule = {
        if (!isSelected) {
            onClickModule(screen)
        }
    }
)
Clicking an already-selected item does nothing, preventing unnecessary navigation.

Customization

Adding New Navigation Items

To add a new item to the navigation bar:
  1. Define the module:
enum class NavigationModule(val hasOwnTab: Boolean = false) {
    // ...
    PROFILE(hasOwnTab = true),  // New module
}
  1. Create the screen:
@Serializable
data object Profile : AppScreens {
    override val module = NavigationModule.PROFILE
    override val hasBottomBar = true
}
  1. Add icon mapping:
private fun getCurrentModuleIcon(module: NavigationModule): Int =
    when (module) {
        NavigationModule.HOME -> R.drawable.ic_cpt_home
        NavigationModule.TEST -> R.drawable.ic_cpt_favorites_outlined
        NavigationModule.PROFILE -> R.drawable.ic_cpt_profile  // New
        else -> R.drawable.ic_cpt_home
    }
  1. Add to screens list:
val screens = listOf(
    AppScreens.Home,
    AppScreens.Test,
    AppScreens.Profile,  // New
)

Styling Customization

CptNavigationBar(
    selectedModule = currentModule,
    modifier = Modifier
        .shadow(8.dp)  // Add elevation
        .background(Color.White),  // Override background
    onClickModule = { /* ... */ }
)
Be cautious when overriding the background color as it may conflict with the theme’s color scheme and affect accessibility.

Accessibility

The component uses Material3’s NavigationBarItem which includes:
  • Proper touch target sizes (minimum 48dp)
  • Content descriptions for screen readers
  • Keyboard navigation support
  • Focus indicators
Icon(
    painter = painterResource(id = iconRes),
    contentDescription = null,  // Handled by NavigationBarItem
)

Best Practices

Limit Items

Keep navigation items between 3-5 for optimal UX. Too many items reduce usability.

Consistent Icons

Use consistent icon styles (all outlined or all filled) for visual harmony.

Clear Labels

While this implementation uses icons only, consider adding text labels for clarity.

State Management

Always track and display the correct selected state to avoid user confusion.

Common Issues

// Check that the screen has hasBottomBar = true
ScreenWrapper(
    showBottomBar = currentScreen.hasBottomBar,  // Must be true
    bottomBar = { CptNavigationBar(...) }
)

Wrong Item Selected

// Ensure selectedModule matches current screen
val currentSelectedModule by 
    currentScreen.getCurrentSelectedModule()
        .collectAsStateWithLifecycle()

CptNavigationBar(
    selectedModule = currentSelectedModule,  // Keep in sync
    // ...
)

See Also

Build docs developers (and LLMs) love