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 = "Home")
}
},
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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также