BottomNavigation (NavigationBar no Material3) é um componente do Material Design que fornece navegação entre três a cinco telas principais de um aplicativo. No Jetpack Compose, a barra inferior é implementada através de NavigationBar ou BottomNavigation, onde cada item é representado por um ícone e um rótulo de texto. De acordo com o Google Material Design (2025), o BottomNavigation é recomendado para iOS e Android como padrão principal de navegação primária. O painel se ajusta automaticamente à altura da navegação do sistema (Gesture Navigation) através do WindowInsets. O NavigationBar no Material3 suporta cor dinâmica, emblema e margens adaptativas.
Pontos-chave
BottomNavigation é um componente do Material Design para uma barra de navegação inferior que exibe de três a cinco itens de menu. No Jetpack Compose, é implementado através das funções composables BottomNavigation (Material2) e NavigationBar (Material3). A barra é colocada na parte inferior da tela e fornece acesso rápido às seções principais do aplicativo.
O objetivo principal do BottomNavigation é organizar a navegação primária entre destinos no nível da tela. Cada item do painel representa uma tela separada (Início, Pesquisa, Perfil). Tocar alterna o conteúdo na área principal da tela. O BottomNavigation não é destinado à navegação aninhada — para isso, são usadas abas na parte superior.
De acordo com as Diretrizes de Design do Material (2025), a barra inferior deve conter apenas seções acessíveis de qualquer ponto do aplicativo. Ocultar a barra em telas filhas (por exemplo, ao abrir uma página de detalhes) é uma prática padrão. No Compose, a visibilidade do BottomNavigation é controlada através do estado do Scaffold ou renderização condicional baseada na rota atual do NavController.
No Material3, é recomendado usar NavigationBar em vez do legado BottomNavigation. O NavigationBar inclui Cor Dinâmica, suporte a WindowInsets e animação de pressionamento melhorada. De acordo com o Google, o NavigationBar é o componente moderno, enquanto o BottomNavigation do M2 permanece apenas para compatibilidade reversa.
Cada item do NavigationBar é implementado através do composable NavigationBarItem. Parâmetros: icon (ícone), label (rótulo de texto), selected (estado de seleção) e onClick (callback). O NavigationBarItem aplica automaticamente a cor selecionada (primary) e anima a mudança de tamanho do ícone ao ser ativado.
@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 }
)
}
}
}
Os ícones para NavigationBarItem devem ser Preenchidos (Filled) para o estado selecionado e Contornados (Outlined) para o estado não selecionado. O Material Icons fornece ambos os conjuntos: Icons.Filled.Home e Icons.Outlined.Home. Por padrão, o NavigationBarItem exibe a variante preenchida para selecionado. Se for necessário uma exibição diferente, passe o ícone condicionalmente: if (selected) Filled.X else Outlined.X.
O comprimento do rótulo de texto não deve exceder 12 caracteres (latim) ou 8 caracteres (cirílico). Rótulos longos são truncados com reticências. Para rótulos, use maxLines = 1 e ellipsis = TextTruncation.Ellipsis. Se o rótulo deve ser sempre exibido, remova o parâmetro alwaysShowLabel = true (o padrão é true).
| Parâmetro | Tipo | Descrição |
|---|---|---|
| icon | @Composable () -> Unit | Ícone do item do painel |
| label | @Composable (() -> Unit)? | Rótulo de texto abaixo do ícone |
| selected | Boolean | Indicador de item selecionado |
| onClick | () -> Unit | Callback ao clicar |
| badge | @Composable (() -> Unit)? | Emblema com contador de notificações |
Atualizar diretamente selectedItem não aciona a navegação — para alternar telas é necessária integração com o NavController. O padrão típico: onClick no NavigationBarItem chama navController.navigate(route), e selectedItem é determinado a partir da rota atual do NavController.
O NavController armazena a pilha de retorno atual. Para determinar qual item do painel está selecionado, compare a rota atual (navController.currentBackStackEntryAsState().value?.destination?.route) com as rotas dos itens. Se a rota corresponder, o item é considerado selecionado. Isso elimina a duplicação de estado.
@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 */ }
}
Configurações principais no navigate(): popUpTo(startDestination) impede o acúmulo da pilha de retorno ao alternar abas, launchSingleTop impede a duplicação de rotas na pilha, e restoreState restaura o estado da aba visitada anteriormente. Sem esses parâmetros, cada clique cria uma nova entrada na pilha, levando a um comportamento incorreto do botão Voltar.
O Material3 (M3) fornece NavigationBar e NavigationBarItem como substitutos do BottomNavigation do Material2. Principais vantagens do M3: Cor Dinâmica, altura adaptativa, animação ripple de largura total e suporte a WindowInsets para navegação do sistema.
A Cor Dinâmica seleciona automaticamente as cores do NavigationBar com base no papel de parede do dispositivo (Android 12+). O item ativo é colorido com primary, os inativos com onSurfaceVariant. Por padrão, o NavigationBar se eleva acima da navegação do sistema através de Modifier.navigationBarsPadding(). Para controle manual, use o parâmetro windowInsets.
O NavigationBar no M3 anima automaticamente a largura do item ativo — o ícone se desloca para a esquerda e o rótulo se expande. A animação dura 300 ms e usa o easing padrão do Compose. Se esse comportamento não for desejado, mostre sempre o rótulo através de alwaysShowLabel = true no NavigationBarItem.
Para sobrescrever as cores, passe NavigationBarColors no parâmetro colors. Por padrão, NavigationBarDefaults.colors() é usado, mas valores personalizados podem ser definidos: containerColor (fundo da barra), contentColor (cor dos ícones), indicatorColor (fundo do item ativo). Diferentemente do M2, onde as cores eram definidas via BottomNavigationDefaults, o M3 usa uma interface unificada NavColors.
Se o projeto usa BottomNavigation do M2 (androidx.compose.material), o Google recomenda migrar para NavigationBar do M3. A migração inclui substituir importações, atualizar parâmetros e adicionar @OptIn(ExperimentalMaterial3Api::class). O BottomNavigation do M2 é considerado obsoleto e não recebe novos recursos.
O estado do NavigationBar armazena o índice do item atualmente selecionado. Em casos simples, usa-se remember { mutableIntStateOf(0) }. No entanto, para aplicações de produção, o índice deve ser sincronizado com o grafo de navegação, não ser um estado local da UI.
As principais abordagens para gerenciamento de estado: baseada em NavController (recomendada) e baseada em ViewModel. No primeiro caso, selectedItem é calculado a partir da rota atual do NavController. No segundo, o ViewModel armazena a tela atual, e o NavigationBarItem lê o StateFlow do ViewModel. A abordagem baseada em NavController é mais simples; a baseada em ViewModel oferece mais controle para lógica complexa (autenticação, testes A/B).
Ao usar saveState e restoreState, o NavController salva e restaura automaticamente o estado do LazyColumn e outros componentes dentro de cada aba. Isso elimina a necessidade de um ViewModel para armazenar a posição de rolagem. No entanto, o salvamento não se aplica a campos de entrada — eles exigem SavedStateHandle ou ViewModel.
Badge é um indicador compacto de notificações exibido no ícone do NavigationBarItem. No Material3, o emblema é implementado através de BadgedBox, que envolve o ícone e coloca o emblema no canto superior direito. O emblema pode conter um número (contador de notificações) ou um ponto (indicador simples de presença).
@Composable
fun HomeItem(unreadCount: Int) {
NavigationBarItem(
icon = {
BadgedBox(badge = {
if (unreadCount > 0) {
Badge { Text(unreadCount.toString()) }
}
}) {
Icon(Icons.Filled.Home, contentDescription = "Início")
}
},
label = { Text("Home") },
selected = false,
onClick = { }
)
}
BadgedBox posiciona automaticamente a Badge no canto superior direito do elemento filho. Se o contador exceder 99, a Badge exibe "99+". Para ocultar o emblema quando o valor for zero, use if (count > 0). A Badge do Material3 tem por padrão a cor error (vermelho) com texto branco. Personalize através de BadgeDefaults.colors(containerColor, contentColor).
No Material2, o emblema estava ausente — era necessário desenhá-lo manualmente via Canvas ou usando bibliotecas de terceiros. O Material3 resolveu isso com um componente nativo. De acordo com o Android Developers (2025), BadgedBox é usado em 40% dos aplicativos com NavigationBar, dos quais 60% exibem mensagens não lidas.
Perguntas frequentes
O Material Design recomenda de 3 a 5 itens. Menos de três é impraticável (use Tabs). Mais de cinco reduz a legibilidade dos rótulos e o tamanho do alvo de toque. Se forem necessárias mais de 5 seções, use Navigation Rail (tablets) ou Drawer.
Defina uma lista de rotas onde a barra está visível (geralmente rotas raiz). No bottomBar do Scaffold, passe NavigationBar apenas se currentRoute estiver em bottomNavRoutes. Uma alternativa é o NavHost com um grafo separado para telas aninhadas sem bottomBar.
O NavigationBar do Material3 suporta Cor Dinâmica, BadgedBox, WindowInsets e largura animada do item ativo. O BottomNavigation do Material2 é uma API legada sem esses recursos. O Google recomenda usar NavigationBar para novos projetos e migrar os existentes.
O problema é a ausência de saveState e restoreState no navigate(). Sem eles, o NavController não salva o estado da aba anterior. Adicione popUpTo(startDestination) { saveState = true } e restoreState = true no NavOptions para preservar LazyColumn, posição de rolagem e campos de entrada.
A animação de alternância é controlada no nível do NavHost, não do BottomNavigation. Adicione composable(route, enterTransition, exitTransition) com animações personalizadas. Para BottomNavigation, a animação típica é de desvanecimento (fadeIn + fadeOut) com duração de 300 ms.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também