BottomNavigation (NavigationBar w Material3) to komponent Material Design zapewniający nawigację między trzema a pięcioma głównymi ekranami aplikacji. W Jetpack Compose dolny panel jest implementowany przez NavigationBar lub BottomNavigation, gdzie każdy element jest reprezentowany przez ikonę i etykietę tekstową. Według Google Material Design (2025), BottomNavigation jest zalecany dla iOS i Android jako podstawowy wzorzec nawigacji podstawowej. Panel automatycznie dostosowuje się do wysokości nawigacji systemowej (Gesture Navigation) poprzez WindowInsets. NavigationBar w Material3 obsługuje dynamic color, badge i adaptacyjne odstępy.
Najważniejsze
BottomNavigation to komponent Material Design dla dolnego panelu nawigacji, wyświetlający od trzech do pięciu pozycji menu. W Jetpack Compose jest zaimplementowany przez funkcje composable BottomNavigation (Material2) i NavigationBar (Material3). Panel umieszczany jest w dolnej części ekranu i zapewnia szybki dostęp do kluczowych sekcji aplikacji.
Głównym zadaniem BottomNavigation jest organizacja nawigacji podstawowej między poziomami ekranów. Każdy element panelu reprezentuje osobny ekran (Home, Search, Profile). Naciśnięcie przełącza zawartość w głównym obszarze ekranu. BottomNavigation nie jest przeznaczony do nawigacji zagnieżdżonej — do tego służą zakładki (Tabs) w górnej części.
Według Material Design Guidelines (2025), dolny panel powinien zawierać tylko te sekcje, które są dostępne z każdego miejsca w aplikacji. Ukrywanie panelu na ekranach podrzędnych (np. przy otwieraniu szczegółowej strony) to standardowa praktyka. W Compose widoczność BottomNavigation jest zarządzana przez stan Scaffold lub warunkowe renderowanie na podstawie bieżącej trasy NavController.
W Material3 zaleca się używanie NavigationBar zamiast Legacy BottomNavigation. NavigationBar zawiera Dynamic Color, obsługę WindowInsets i ulepszoną animację naciśnięcia. Według Google, NavigationBar jest perspektywicznym komponentem, a BottomNavigation z M2 pozostaje tylko dla wstecznej kompatybilności.
Każdy element NavigationBar jest implementowany przez composable NavigationBarItem. Parametry elementu: icon (ikona), label (etykieta tekstowa), selected (stan wyboru) i onClick (callback). NavigationBarItem automatycznie stosuje wybrany kolor (primary) i animację zmiany rozmiaru ikony przy aktywacji.
@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 }
)
}
}
}
Ikony dla NavigationBarItem powinny być Filled dla stanu wybranego i Outlined dla niewybranego. Material Icons udostępniają oba zestawy: Icons.Filled.Home i Icons.Outlined.Home. Domyślnie NavigationBarItem wyświetla wariant filled dla selected. Jeśli potrzebne jest różne wyświetlanie, przekazuj ikonę warunkowo: if (selected) Filled.X else Outlined.X.
Długość etykiety tekstowej elementu — nie więcej niż 12 znaków (alfabet łaciński) lub 8 znaków (cyrylica). Długie etykiety są przycinane z wielokropkiem. Dla etykiet używaj maxLines = 1 i ellipsis = TextTruncation.Ellipsis. Jeśli etykieta ma być zawsze widoczna, ustaw parametr alwaysShowLabel = true (domyślnie true).
| Parametr | Typ | Opis |
|---|---|---|
| icon | @Composable () -> Unit | Ikona elementu panelu |
| label | @Composable (() -> Unit)? | Etykieta tekstowa pod ikoną |
| selected | Boolean | Flaga wybranego elementu |
| onClick | () -> Unit | Callback przy naciśnięciu |
| badge | @Composable (() -> Unit)? | Badge z liczbą powiadomień |
Bezpośrednia aktualizacja selectedItem nie przekierowuje nawigacji — do przełączania ekranów potrzebna jest integracja z NavController. Typowy wzorzec: onClick w NavigationBarItem wywołuje navController.navigate(route), a selectedItem jest określany na podstawie bieżącej trasy NavController.
NavController przechowuje bieżący back stack. Aby określić, który element panelu jest wybrany, porównaj bieżącą trasę (navController.currentBackStackEntryAsState().value?.destination?.route) z trasami elementów. Jeśli trasa się zgadza — element jest uważany za wybrany. Eliminuje to dublowanie stanu.
@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 */ }
}
Kluczowe ustawienia w navigate(): popUpTo(startDestination) zapobiega gromadzeniu się back stack przy przełączaniu zakładek, launchSingleTop eliminuje dublowanie trasy w stosie, a restoreState przywraca stan poprzedniej wizyty w zakładce. Bez tych parametrów każde naciśnięcie tworzy nowy wpis w stosie, co prowadzi do nieprawidłowego działania przycisku Wstecz.
Material3 (M3) udostępnia NavigationBar i NavigationBarItem jako zamiennik BottomNavigation z Material2. Główne zalety M3: Dynamic Color, adaptacyjna wysokość, animacja ripple na całej szerokości elementu i obsługa WindowInsets do uwzględnienia nawigacji systemowej.
Dynamic Color automatycznie dobiera kolory NavigationBar na podstawie tapety urządzenia (Android 12+). Aktywny element jest kolorowany na primary, nieaktywne — na onSurfaceVariant. Domyślnie NavigationBar unosi się nad nawigacją systemową przez Modifier.navigationBarsPadding(). Do ręcznego sterowania użyj parametru windowInsets.
NavigationBar w M3 automatycznie animuje szerokość aktywnego elementu — ikona przesuwa się w lewo, a etykieta rozszerza się. Animacja trwa 300 ms i używa standardowego easingu Compose. Jeśli takie zachowanie jest niepożądane, ustaw zawsze widoczną etykietę przez alwaysShowLabel = true w NavigationBarItem.
Aby zmienić kolory, przekaż NavigationBarColors w parametrze colors. Domyślnie używane jest NavigationBarDefaults.colors(), ale można ustawić własne: containerColor (tło panelu), contentColor (kolor ikon), indicatorColor (tło aktywnego elementu). W przeciwieństwie do M2, gdzie kolory były ustawiane przez BottomNavigationDefaults, w M3 używany jest jednolity interfejs NavColors.
Jeśli w projekcie używany jest BottomNavigation z M2 (androidx.compose.material), Google zaleca migrację do M3 NavigationBar. Migracja obejmuje wymianę importów, aktualizację parametrów i dodanie @OptIn(ExperimentalMaterial3Api::class). BottomNavigation z M2 jest uważany za przestarzały i nie otrzymuje nowych funkcji.
Stan NavigationBar przechowuje indeks aktualnie wybranego elementu. W prostych przypadkach używany jest remember { mutableIntStateOf(0) }. Jednak w aplikacjach produkcyjnych indeks powinien być synchronizowany z grafem nawigacji, a nie być lokalnym stanem UI.
Podstawowe podejścia do zarządzania stanem: NavController-based (zalecane) i ViewModel-based. W pierwszym przypadku selectedItem jest obliczany z bieżącej trasy NavController. W drugim — ViewModel przechowuje bieżący ekran, a NavigationBarItem odczytuje StateFlow z ViewModel. NavController-based jest prostsze, ViewModel-based daje więcej kontroli przy złożonej logice (autoryzacja, testy A/B).
Przy użyciu saveState i restoreState NavController automatycznie zapisuje i przywraca stan LazyColumn i innych komponentów wewnątrz każdej zakładki. Eliminuje to potrzebę stosowania ViewModel do przechowywania pozycji scrolla. Jednak zapisywanie nie ma zastosowania dla pól wejściowych — wymagają one SavedStateHandle lub ViewModel.
Badge to kompaktowy wskaźnik powiadomień wyświetlany na ikonie NavigationBarItem. W Material3 badge jest implementowany przez BadgedBox, który otacza ikonę i umieszcza badge w prawym górnym rogu. Badge może zawierać liczbę (ilość powiadomień) lub kropkę (proste wskazanie na obecność).
@Composable
fun HomeItem(unreadCount: Int) {
NavigationBarItem(
icon = {
BadgedBox(badge = {
if (unreadCount > 0) {
Badge { Text(unreadCount.toString()) }
}
}) {
Icon(Icons.Filled.Home, contentDescription = "Strona główna")
}
},
label = { Text("Home") },
selected = false,
onClick = { }
)
}
BadgedBox automatycznie pozycjonuje Badge w prawym górnym rogu elementu podrzędnego. Jeśli liczba przekracza 99, Badge wyświetla „99+“. Aby ukryć badge przy zerowej wartości, użyj warunku if (count > 0). Badge z Material3 domyślnie ma kolor error (czerwony) z białym tekstem. Dostosowanie przez BadgeDefaults.colors(containerColor, contentColor).
W Material2 badge nie istniał — trzeba było rysować go ręcznie przez Canvas lub używać bibliotek zewnętrznych. Material3 rozwiązał ten problem natywnym komponentem. Według Android Developers (2025), BadgedBox jest używany w 40% aplikacji z NavigationBar, z czego 60% — do wyświetlania nieprzeczytanych wiadomości.
Często zadawane pytania
Material Design zaleca od 3 do 5 elementów. Mniej niż trzy — niecelowe (lepiej użyć Tabs). Więcej niż pięć — pogarsza czytelność etykiet i zmniejsza touch-arget. Jeśli potrzeba więcej niż 5 sekcji, użyj Navigation Rail (tablety) lub Drawer.
Określ listę tras, na których panel jest widoczny (zwykle główne). W Scaffold bottomBar przekazuj NavigationBar tylko jeśli currentRoute in bottomNavRoutes. Alternatywa — NavHost z osobnym grafem dla ekranów zagnieżdżonych bez bottomBar.
NavigationBar z Material3 obsługuje Dynamic Color, BadgedBox, WindowInsets i animowaną szerokość aktywnego elementu. BottomNavigation z Material2 to stare API bez tych funkcji. Google zaleca używanie NavigationBar dla nowych projektów i migrację istniejących.
Problem polega na braku saveState i restoreState w navigate(). Bez nich NavController nie zapisuje stanu poprzedniej zakładki. Dodaj popUpTo(startDestination) { saveState = true } i restoreState = true w NavOptions, aby zachować LazyColumn, scroll i pola wejściowe.
Animacja przełączania jest kontrolowana na poziomie NavHost, a nie BottomNavigation. Dodaj composable(route, enterTransition, exitTransition) z niestandardowymi animacjami. Dla BottomNavigation typowa jest animacja fadingowa (fadeIn + fadeOut) o czasie trwania 300ms.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również