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 — організація первинної навігації між screen-рівнями. Кожен елемент панелі представляє окремий екран (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 відновлює стан попереднього відвідування вкладки. Без цих параметрів кожне натискання створює новий entry в стеку, що призводить до некоректної поведінки кнопки Back.
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. Міграція включає заміну імпортів, оновлення параметрів та додавання @OptIn(ExperimentalMaterial3Api::class). BottomNavigation з M2 вважається застарілим і не отримує нових функцій.
Стан NavigationBar зберігає індекс поточного вибраного елемента. У простих випадках використовується remember { mutableIntStateOf(0) }. Однак для production-застосунків індекс повинен синхронізуватися з навігаційним графом, а не бути локальним станом 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-таргет. Якщо потрібно більше 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також