BottomNavigation (NavigationBar у Material3) је компонента Material Design-а која обезбеђује навигацију између три до пет главних екрана апликације. У Jetpack Compose-у доњи панел се имплементира кроз NavigationBar или BottomNavigation, где је сваки елемент представљен иконом и текстуалном ознаком. Према Google Material Design (2025), BottomNavigation се препоручује за iOS и Android као основни образац примарне навигације. Панел се аутоматски прилагођава висини системске навигације (Gesture Navigation) путем WindowInsets. NavigationBar у Material3 подржава dynamic color, badge и прилагодљиве размаке.
Главно
BottomNavigation је компонента Material Design-а за доњи панел навигације који приказује од три до пет ставки менија. У Jetpack Compose-у је имплементиран кроз composable функције BottomNavigation (Material2) и NavigationBar (Material3). Панел се поставља у доњем делу екрана и обезбеђује брз приступ кључним секцијама апликације.
Основни задатак BottomNavigation-а је организовање примарне навигације између нивоа екрана. Сваки елемент панела представља посебан екран (Home, Search, Profile). Притиском се пребацује садржај у главној области екрана. BottomNavigation није намењен за угњеждену навигацију — за то се користе картице (Tabs) у горњем делу.
Према Material Design Guidelines (2025), доњи панел треба да садржи само оне секције које су доступне из било које тачке апликације. Скривање панела на подређеним екранима (нпр. при отварању странице детаља) је стандардна пракса. У Compose-у, видљивост BottomNavigation-а се управља кроз стање Scaffold-а или условно приказивање на основу тренутне руте NavController-а.
У Material3 се препоручује коришћење NavigationBar-а уместо Legacy BottomNavigation-а. NavigationBar укључује Dynamic Color, подршку за WindowInsets и побољшану анимацију притиска. Према Google-у, NavigationBar је перспективна компонента, а BottomNavigation из M2 остаје само за уназадну компатибилност.
Сваки елемент NavigationBar-а се имплементира кроз composable NavigationBarItem. Параметри елемента: icon (икона), label (текстуална ознака), selected (стање избора) и onClick (повратни позив). NavigationBarItem аутоматски примењује изабрану боју (primary) и анимацију промене величине иконе при активацији.
@Composable
fun BottomNavBar() {
val items = listOf(
Screen.Home,
Screen.Search,
Screen.Profile
)
var selectedItem by remember { mutableIntStateOf(0) }
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
Иконе за NavigationBarItem треба да буду Filled за изабрано стање и Outlined за неизабрано. Material Icons пружају оба скупа: Icons.Filled.Home и Icons.Outlined.Home. Подразумевано, NavigationBarItem приказује filled варијанту за selected. Ако је потребан другачији приказ, преносите икону условно: if (selected) Filled.X else Outlined.X.
Дужина текстуалне ознаке елемента — не више од 12 знакова (латиница) или 8 знакова (ћирилица). Дуге ознаке се скраћују са три тачке. За ознаке користите maxLines = 1 и ellipsis = TextTruncation.Ellipsis. Ако је ознака увек потребна, поставите параметар alwaysShowLabel = true (подразумевано true).
| Параметар | Тип | Опис |
|---|---|---|
| icon | @Composable () -> Unit | Икона елемента панела |
| label | @Composable (() -> Unit)? | Текстуална ознака испод иконе |
| selected | Boolean | Заставица изабраног елемента |
| onClick | () -> Unit | Повратни позив при притиску |
| badge | @Composable (() -> Unit)? | Значка са бројем обавештења |
Директно ажурирање selectedItem не усмерава навигацију — за пребацивање екрана потребна је интеграција са NavController-ом. Типичан образац: onClick у NavigationBarItem позива navController.navigate(route), а selectedItem се одређује из тренутне руте NavController-а.
NavController чува тренутни back stack. Да бисте одредили који елемент панела је изабран, упоредите тренутну руту (navController.currentBackStackEntryAsState().value?.destination?.route) са рутама елемената. Ако се рута поклапа — елемент се сматра изабраним. Ово елиминише дуплирање стања.
@Composable
fun MainScreen(navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { /* content */ }
}
Кључна подешавања у navigate(): popUpTo(startDestination) спречава нагомилавање back stack-а при пребацивању картица, launchSingleTop искључује дуплирање руте у стеку, а restoreState обнавља стање претходне посете картици. Без ових параметара, сваки притисак ствара нови унос у стек, што доводи до неисправног понашања дугмада Назад.
Material3 (M3) пружа NavigationBar и NavigationBarItem као замену за BottomNavigation из Material2. Главне предности M3: Dynamic Color, адаптивна висина, анимација ripple на целој ширини елемента и подршка за WindowInsets за урачунавање системске навигације.
Dynamic Color аутоматски бира боје NavigationBar-а на основу позадине уређаја (Android 12+). Активни елемент се боји у primary, неактивни — у onSurfaceVariant. Подразумевано, NavigationBar се подиже изнад системске навигације кроз Modifier.navigationBarsPadding(). За ручно управљање користите параметар windowInsets.
NavigationBar у M3 аутоматски анимира ширину активног елемента — икона се помера улево, а ознака се шири. Анимација траје 300 ms и користи стандардни easing Compose-а. Ако је ово понашање непожељно, поставите увек видљиву ознаку кроз alwaysShowLabel = true у NavigationBarItem-у.
За претпостављање боја пренесите NavigationBarColors у параметру colors. Подразумевано се користи NavigationBarDefaults.colors(), али можете поставити своје: containerColor (позадина панела), contentColor (боја икона), indicatorColor (позадина активног елемента). За разлику од M2, где су боје постављане кроз BottomNavigationDefaults, у M3 се користи јединствени интерфејс NavColors.
Ако се у пројекту користи BottomNavigation из M2 (androidx.compose.material), Google препоручује миграцију на M3 NavigationBar. Миграција укључује замену import-а, ажурирање параметара и додавање @OptIn(ExperimentalMaterial3Api::class). BottomNavigation из M2 се сматра застарелим и не добија нове функције.
Стање NavigationBar-а чува индекс тренутно изабраног елемента. У једноставним случајевима користи се remember { mutableIntStateOf(0) }. Међутим, за производне апликације индекс треба да буде синхронизован са навигационим графом, а не да буде локално стање UI-ја.
Основни приступи управљању стањем: NavController-based (препоручује се) и ViewModel-based. У првом случају selectedItem се израчунава из тренутне руте NavController-а. У другом — ViewModel чува тренутни екран, а NavigationBarItem чита StateFlow из ViewModel-а. NavController-based је једноставнији, ViewModel-based даје више контроле при сложеној логици (ауторизација, A/B тестови).
При коришћењу saveState и restoreState, NavController аутоматски чува и обнавља стање LazyColumn-а и других компоненти унутар сваке картице. Ово елиминише потребу за ViewModel-ом за чување позиције скроловања. Међутим, чување се не примењује на поља за унос — она захтевају SavedStateHandle или ViewModel.
Badge је компактни индикатор обавештења који се приказује на икони NavigationBarItem-а. У Material3 значка се имплементира кроз BadgedBox, који обавија икону и поставља значку у горњи десни угао. Badge може да садржи број (количину обавештења) или тачку (једноставну назнаку присуства).
@Composable
fun HomeItem(unreadCount: Int) {
NavigationBarItem(
icon = {
BadgedBox(badge = {
if (unreadCount > 0) {
Badge { Text(unreadCount.toString()) }
}
}) {
Icon(Icons.Filled.Home, contentDescription = "Почетна")
}
},
label = { Text("Home") },
selected = false,
onClick = { }
)
}
BadgedBox аутоматски позиционира Badge у горњи десни угао подређеног елемента. Ако број прелази 99, Badge приказује „99+“. За скривање значке при нултој вредности користите услов if (count > 0). Badge из Material3 подразумевано има боју error (црвена) са белим текстом. Прилагођавање кроз BadgeDefaults.colors(containerColor, contentColor).
У Material2 значка није постојала — морала се ручно цртати кроз Canvas или користити библиотеке трећих страна. Material3 је решио овај проблем изворном компонентом. Према Android Developers (2025), BadgedBox се користи у 40% апликација са NavigationBar-ом, од чега 60% — за приказ непрочитаних порука.
Често постављана питања
Material Design препоручује од 3 до 5 елемената. Мање од три — нецелисходно (боље је користити Tabs). Више од пет — погоршава читљивост ознака и смањује touch-target. Ако је потребно више од 5 секција, користите Navigation Rail (таблети) или Drawer.
Одредите листу рута где је панел видљив (обично корене). У Scaffold bottomBar преносите NavigationBar само ако је currentRoute in bottomNavRoutes. Алтернатива — NavHost са посебним графом за угњеждене екране без bottomBar-а.
NavigationBar из Material3 подржава Dynamic Color, BadgedBox, WindowInsets и анимирану ширину активног елемента. BottomNavigation из Material2 је стари API без ових функција. Google препоручује коришћење NavigationBar-а за нове пројекте и миграцију постојећих.
Проблем је у недостатку saveState и restoreState у navigate(). Без њих NavController не чува стање претходне картице. Додајте popUpTo(startDestination) { saveState = true } и restoreState = true у NavOptions за очување LazyColumn-а, скроловања и поља за унос.
Анимација пребацивања се контролише на нивоу NavHost-а, а не BottomNavigation-а. Додајте composable(route, enterTransition, exitTransition) са прилагођеним анимацијама. За BottomNavigation је типична fading анимација (fadeIn + fadeOut) у трајању од 300ms.
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође