BottomNavigation (NavigationBar in Material3) è un componente Material Design che fornisce navigazione tra tre e cinque schermate principali di un'applicazione. In Jetpack Compose, la barra inferiore viene implementata tramite NavigationBar o BottomNavigation, dove ogni elemento è rappresentato da un'icona e un'etichetta di testo. Secondo Google Material Design (2025), BottomNavigation è raccomandato per iOS e Android come pattern principale di navigazione primaria. Il pannello si adatta automaticamente all'altezza della navigazione di sistema (Gesture Navigation) tramite WindowInsets. NavigationBar in Material3 supporta colore dinamico, badge e margini adattivi.
Punti chiave
BottomNavigation è un componente Material Design per una barra di navigazione inferiore che visualizza da tre a cinque voci di menu. In Jetpack Compose, viene implementato tramite le funzioni composable BottomNavigation (Material2) e NavigationBar (Material3). La barra è posizionata nella parte inferiore dello schermo e fornisce accesso rapido alle sezioni principali dell'applicazione.
Lo scopo principale di BottomNavigation è organizzare la navigazione primaria tra le destinazioni a livello di schermata. Ogni elemento del pannello rappresenta una schermata separata (Home, Ricerca, Profilo). Toccando si cambia il contenuto nell'area principale dello schermo. BottomNavigation non è destinato alla navigazione annidata — per questo si usano le schede nella parte superiore.
Secondo le Linee Guida di Material Design (2025), la barra inferiore dovrebbe contenere solo sezioni accessibili da qualsiasi punto dell'applicazione. Nascondere la barra nelle schermate figlie (ad esempio, all'apertura di una pagina di dettaglio) è una pratica standard. In Compose, la visibilità di BottomNavigation è controllata tramite lo stato di Scaffold o il rendering condizionale basato sulla route corrente di NavController.
In Material3, si raccomanda di utilizzare NavigationBar invece della legacy BottomNavigation. NavigationBar include Colore Dinamico, supporto WindowInsets e animazione di pressione migliorata. Secondo Google, NavigationBar è il componente moderno, mentre BottomNavigation di M2 rimane solo per retrocompatibilità.
Ogni elemento di NavigationBar viene implementato tramite il composable NavigationBarItem. Parametri: icon (icona), label (etichetta di testo), selected (stato di selezione) e onClick (callback). NavigationBarItem applica automaticamente il colore selezionato (primary) e anima il cambiamento di dimensione dell'icona all'attivazione.
@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 }
)
}
}
}
Le icone per NavigationBarItem dovrebbero essere piene (Filled) per lo stato selezionato e delineate (Outlined) per lo stato non selezionato. Material Icons fornisce entrambi i set: Icons.Filled.Home e Icons.Outlined.Home. Per impostazione predefinita, NavigationBarItem mostra la variante piena per selezionato. Se è necessaria una visualizzazione diversa, passa l'icona condizionalmente: if (selected) Filled.X else Outlined.X.
La lunghezza dell'etichetta di testo non deve superare 12 caratteri (latino) o 8 caratteri (cirillico). Le etichette lunghe vengono troncate con puntini di sospensione. Per le etichette, utilizzare maxLines = 1 e ellipsis = TextTruncation.Ellipsis. Se l'etichetta deve essere sempre visualizzata, rimuovere il parametro alwaysShowLabel = true (il valore predefinito è true).
| Parametro | Tipo | Descrizione |
|---|---|---|
| icon | @Composable () -> Unit | Icona dell'elemento del pannello |
| label | @Composable (() -> Unit)? | Etichetta di testo sotto l'icona |
| selected | Boolean | Indicatore di elemento selezionato |
| onClick | () -> Unit | Callback al clic |
| badge | @Composable (() -> Unit)? | Badge con contatore di notifiche |
Aggiornare direttamente selectedItem non attiva la navigazione — per cambiare schermata è necessaria l'integrazione con NavController. Il pattern tipico: onClick in NavigationBarItem chiama navController.navigate(route), e selectedItem è determinato dalla route corrente di NavController.
NavController memorizza lo stack di ritorno corrente. Per determinare quale elemento del pannello è selezionato, confrontare la route corrente (navController.currentBackStackEntryAsState().value?.destination?.route) con le route degli elementi. Se la route corrisponde, l'elemento è considerato selezionato. Questo elimina la duplicazione dello stato.
@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 */ }
}
Impostazioni chiave in navigate(): popUpTo(startDestination) impedisce l'accumulo dello stack di ritorno durante il cambio di schede, launchSingleTop impedisce la duplicazione delle route nello stack, e restoreState ripristina lo stato della scheda visitata in precedenza. Senza questi parametri, ogni clic crea una nuova voce nello stack, portando a un comportamento errato del pulsante Indietro.
Material3 (M3) fornisce NavigationBar e NavigationBarItem come sostituti di BottomNavigation di Material2. Principali vantaggi di M3: Colore Dinamico, altezza adattiva, animazione ripple a larghezza intera e supporto WindowInsets per la navigazione di sistema.
Il Colore Dinamico seleziona automaticamente i colori di NavigationBar in base allo sfondo del dispositivo (Android 12+). L'elemento attivo viene colorato con primary, quelli inattivi con onSurfaceVariant. Per impostazione predefinita, NavigationBar si solleva sopra la navigazione di sistema tramite Modifier.navigationBarsPadding(). Per il controllo manuale, utilizzare il parametro windowInsets.
NavigationBar in M3 anima automaticamente la larghezza dell'elemento attivo — l'icona si sposta a sinistra e l'etichetta si espande. L'animazione dura 300 ms e utilizza l'easing standard di Compose. Se questo comportamento non è desiderato, mostra sempre l'etichetta tramite alwaysShowLabel = true in NavigationBarItem.
Per sovrascrivere i colori, passare NavigationBarColors nel parametro colors. Per impostazione predefinita, viene utilizzato NavigationBarDefaults.colors(), ma è possibile impostare valori personalizzati: containerColor (sfondo della barra), contentColor (colore delle icone), indicatorColor (sfondo dell'elemento attivo). A differenza di M2, dove i colori erano impostati tramite BottomNavigationDefaults, M3 utilizza un'interfaccia unificata NavColors.
Se il progetto utilizza BottomNavigation di M2 (androidx.compose.material), Google consiglia di migrare a NavigationBar di M3. La migrazione include la sostituzione degli import, l'aggiornamento dei parametri e l'aggiunta di @OptIn(ExperimentalMaterial3Api::class). BottomNavigation di M2 è considerato deprecato e non riceve nuove funzionalità.
Lo stato di NavigationBar memorizza l'indice dell'elemento attualmente selezionato. In casi semplici, si usa remember { mutableIntStateOf(0) }. Tuttavia, per applicazioni di produzione, l'indice deve essere sincronizzato con il grafo di navigazione, non essere uno stato locale dell'interfaccia utente.
I principali approcci alla gestione dello stato: basato su NavController (raccomandato) e basato su ViewModel. Nel primo caso, selectedItem viene calcolato dalla route corrente di NavController. Nel secondo, il ViewModel memorizza la schermata corrente e NavigationBarItem legge lo StateFlow dal ViewModel. L'approccio basato su NavController è più semplice; quello basato su ViewModel offre più controllo per logiche complesse (autenticazione, test A/B).
Quando si utilizza saveState e restoreState, NavController salva e ripristina automaticamente lo stato di LazyColumn e altri componenti all'interno di ogni scheda. Questo elimina la necessità di un ViewModel per memorizzare la posizione di scorrimento. Tuttavia, il salvataggio non si applica ai campi di input — richiedono SavedStateHandle o ViewModel.
Badge è un indicatore compatto di notifiche visualizzato sull'icona di NavigationBarItem. In Material3, il badge viene implementato tramite BadgedBox, che avvolge l'icona e posiziona il badge nell'angolo superiore destro. Il badge può contenere un numero (conteggio notifiche) o un punto (semplice indicatore di presenza).
@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 posiziona automaticamente il Badge nell'angolo superiore destro dell'elemento figlio. Se il conteggio supera 99, il Badge visualizza "99+". Per nascondere il badge quando il valore è zero, utilizzare if (count > 0). Il Badge di Material3 ha per impostazione predefinita il colore error (rosso) con testo bianco. Personalizzare tramite BadgeDefaults.colors(containerColor, contentColor).
In Material2, il badge era assente — doveva essere disegnato manualmente tramite Canvas o utilizzando librerie di terze parti. Material3 ha risolto questo problema con un componente nativo. Secondo Android Developers (2025), BadgedBox è utilizzato nel 40% delle app con NavigationBar, di cui il 60% mostra messaggi non letti.
Domande frequenti
Material Design raccomanda da 3 a 5 elementi. Meno di tre non è pratico (usa invece le schede). Più di cinque riduce la leggibilità delle etichette e la dimensione del target tattile. Se sono necessarie più di 5 sezioni, usa Navigation Rail (tablet) o Drawer.
Definisci un elenco di route in cui la barra è visibile (di solito route radice). Nel bottomBar di Scaffold, passa NavigationBar solo se currentRoute è in bottomNavRoutes. Un'alternativa è NavHost con un grafo separato per schermate annidate senza bottomBar.
NavigationBar di Material3 supporta Colore Dinamico, BadgedBox, WindowInsets e larghezza animata dell'elemento attivo. BottomNavigation di Material2 è un'API legacy senza queste funzionalità. Google raccomanda di usare NavigationBar per i nuovi progetti e migrare quelli esistenti.
Il problema è l'assenza di saveState e restoreState in navigate(). Senza di essi, NavController non salva lo stato della scheda precedente. Aggiungi popUpTo(startDestination) { saveState = true } e restoreState = true in NavOptions per preservare LazyColumn, posizione di scorrimento e campi di input.
L'animazione di cambio è controllata a livello di NavHost, non di BottomNavigation. Aggiungi composable(route, enterTransition, exitTransition) con animazioni personalizzate. Per BottomNavigation, un'animazione di dissolvenza (fadeIn + fadeOut) con durata di 300 ms è tipica.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche