Se você já construiu uma tela com Jetpack Compose, em algum momento precisa responder a uma pergunta inevitável: como o usuário sai daqui e vai para lá? A resposta canônica, estável e amplamente adotada em produção é o Navigation Compose — o componente do AndroidX que traz NavHost, NavController e grafos de navegação declarativos para o mundo das funções composáveis.
Neste guia completo, você vai dominar o Navigation Compose do zero ao avançado: configuração, NavHost, NavController, rotas type-safe, argumentos, bottom navigation, grafos aninhados, deep links, salvamento de estado e boas práticas de arquitetura. Se você está começando, leia antes o tutorial de Compose para iniciantes e o conteúdo sobre Kotlin para Android para fixar a base.
O que é o Navigation Compose?
O Navigation Compose é a integração oficial da biblioteca AndroidX Navigation com o Jetpack Compose. Enquanto a navegação clássica nasceu no mundo de Activities e Fragments (NavigationUI, NavHostFragment, gráficos em XML), esta versão substitui o XML por composables e expõe o NavController como algo que você segura no nível do seu app.
Os pilares são três:
NavController— o objeto central que conhece a back stack, o destino atual e expõenavigate()epopBackStack().NavHost— um composable que desenha o destino correspondente ao estado atual doNavController. É nele que você registra todas as telas.- Destination / Route — a definição de cada tela, que pode receber argumentos tipados e ser protegida por lógica.
A grande vantagem sobre soluções caseiras (listas de estado com when) é que o Navigation Compose resolve, de forma testada, os problemas difíceis que aparecem em todo app real: back stack, state saving, deep links, transições, resultado entre telas e integração com o botão de voltar do sistema.
Dependências e configuração
No seu build.gradle.kts (módulo :app), adicione o artefato navigation-compose:
dependencies {
val navVersion = "2.8.5"
implementation("androidx.navigation:navigation-compose:$navVersion")
}
Se você gerencia versões em catálogo — o que recomendamos, veja o guia de Version Catalog — basta registrar a lib no libs.versions.toml:
[versions]
navigationCompose = "2.8.5"
[libraries]
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
implementation(libs.androidx.navigation.compose)
NavController: o coração da navegação
A regra de ouro do Navigation Compose é uma única instância de NavController por app, geralmente retida no nível mais alto da sua UI. Em Compose, você a cria com rememberNavController():
@Composable
fun KotlinBrasilApp() {
val navController = rememberNavController()
KotlinBrasilTheme {
Scaffold { innerPadding ->
KotlinNavHost(
navController = navController,
modifier = Modifier.padding(innerPadding),
)
}
}
}
Por que segurar o NavController lá em cima? Porque a barra inferior, a top bar e o NavHost precisam enxergar o mesmo controlador para reagir ao destino atual (por exemplo, esconder a bottom bar na tela de login). Em apps que usam injeção de dependência, o guia de Hilt com Compose mostra como fornecer ViewModels que conversam com a navegação sem acoplar UI a regras de negócio.
NavHost: registrando destinos
O NavHost recebe o NavController, o destino inicial e um bloco composable { } para cada tela:
@Composable
fun KotlinNavHost(
navController: NavController,
modifier: Modifier = Modifier,
) {
NavHost(
navController = navController,
startDestination = "home",
modifier = modifier,
) {
composable("home") {
HomeScreen(
onOpenArticle = { id -> navController.navigate("article/$id") },
)
}
composable(
route = "article/{articleId}",
arguments = listOf(navArgument("articleId") { type = NavType.StringType }),
) { backStackEntry ->
val articleId = backStackEntry.arguments?.getString("articleId").orEmpty()
ArticleScreen(articleId = articleId)
}
}
}
Aqui já aparecem três conceitos chave: a rota ("home", "article/{articleId}"), o placeholder de argumento ({articleId}) e o navArgument que define o tipo esperado. O backStackEntry entregue ao composable de destino carrega os argumentos já parseados.
Rotas type-safe com objetos serializáveis
A partir do AndroidX Navigation 2.8.0, você pode abandonar as strings mágicas e usar rotas serializáveis, que dão segurança de tipos em tempo de compilação. Defina cada destino como um object ou data class marcado com @Serializable (conceito explicado em detalhe no nosso glossário de Serialization e no artigo de serialização avançada e polimorfismo):
import kotlinx.serialization.Serializable
@Serializable object Home
@Serializable data class Article(val articleId: String)
@Serializable data class Search(val query: String = "")
@Composable
fun KotlinNavHost(navController: NavController) {
NavHost(navController, startDestination = Home) {
composable<Home> {
HomeScreen(onOpenArticle = { id ->
navController.navigate(Article(id))
})
}
composable<Article> { backStackEntry ->
val article: Article = backStackEntry.toRoute()
ArticleScreen(articleId = article.articleId)
}
composable<Search> { backStackEntry ->
val search: Search = backStackEntry.toRoute()
SearchScreen(initialQuery = search.query)
}
}
}
As vantagens são diretas: o compilador reclama se você passar o tipo errado, não há concatenação de string para montar URLs e os argumentos opcionais viram parâmetros com valor padrão. Sempre que possível, prefira esta abordagem em vez de rotas como string.
Argumentos opcionais e valores padrão
Argumentos opcionais precisam de defaultValue e de nullable = true. Com rotas type-safe, isso desaparece — basta usar val query: String = "". Se você ainda está em strings, o equivalente é:
composable(
route = "search?q={query}",
arguments = listOf(navArgument("query") {
type = NavType.StringType
defaultValue = ""
nullable = true
}),
)
Bottom navigation integrada
Um padrão clássico é casar NavigationBar (Material 3) com o NavHost. O truque é manter a back stack do destino raiz de cada aba para preservar estado ao trocar de tab:
@Composable
fun MainScreen(navController: NavController) {
val items = listOf(Tab.Home, Tab.Search, Tab.Profile)
Scaffold(
bottomBar = {
val currentBackStack by navController.currentBackStackEntryAsState()
val currentDestination = currentBackStack?.destination
NavigationBar {
items.forEach { tab ->
NavigationBarItem(
selected = currentDestination?.hierarchy?.any { it.route == tab.route } == true,
onClick = {
navController.navigate(tab.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(tab.icon, contentDescription = tab.label) },
label = { Text(tab.label) },
)
}
}
},
) { innerPadding ->
KotlinNavHost(navController, Modifier.padding(innerPadding))
}
}
Os detalhes que fazem a diferença são: popUpTo com saveState = true, launchSingleTop = true e restoreState = true. Sem eles, toda vez que o usuário volta a uma aba você recria a tela do zero e perde a posição de rolagem. O design da barra segue as recomendações de Material 3 Expressive com Compose e os layouts de Compose.
Grafos aninhados
Para fluxos fechados (checkout, onboarding, wizard), use navigation { } dentro do NavHost para criar um subgrafo com sua própria rota de entrada:
navigation(startDestination = Checkout.Cart, route = "checkout") {
composable<Checkout.Cart> { CartScreen(onNext = { navController.navigate(Checkout.Address) }) }
composable<Checkout.Address> { AddressScreen(onNext = { navController.navigate(Checkout.Payment) }) }
composable<Checkout.Payment> { PaymentScreen(onDone = { navController.popBackStack() }) }
}
Assim você navega para "checkout" e o usuário fica preso ao fluxo até terminar — o botão de voltar do sistema respeita a back stack interna.
Deep links e App Links
O NavHost aceita deepLinks por destino, conectando rotas internas a URLs reais (o que casca bem com o guia de App Links e deep links com Kotlin):
composable<Article>(
deepLinks = listOf(navDeepLink { uriPattern = "https://kotlin.dev.br/artigos/{articleId}" }),
) { entry ->
val article: Article = entry.toRoute()
ArticleScreen(article.articleId)
}
Depois registre o intent-filter correspondente no AndroidManifest.xml para que links externos (notificações, e-mails, busca) abram direto na tela certa.
Resultado entre telas
Para devolver um valor (selecionar um item, criar algo), use savedStateHandle:
// Tela que pede um resultado
val result = navController.currentBackStackEntry
?.savedStateHandle
?.getStateFlow<String>("selectedCategory", "")
?.collectAsState()
navController.navigate("categoryPicker")
// Tela que devolve o resultado
navController.previousBackStackEntry?.savedStateHandle?.set("selectedCategory", "coroutines")
navController.popBackStack()
Como o savedStateHandle sobrevive a mudanças de configuração, o resultado chega de forma confiável. Para testar esses fluxos reativos, o guia de testes com Turbine e StateFlow é leitura obrigatória.
Estado, back stack e boas práticas
Algumas regras que evitam bugs difíceis em produção:
- Nunca passe o
NavControllerpara dentro de ViewModels ou repositórios. A navegação é uma preocupação de UI. - Centralize as rotas em um único lugar (objetos
@Serializableou constantes) para evitar strings duplicadas. - Use
popUpTocom cuidado: esquecer osaveStateé a causa nº 1 de telas que “resetam” ao trocar de aba. - Para telas que devem sobreviver a morte do processo, confie no
savedStateHandlee persista dados críticos em DataStore, não emremember. - Modele estados de UI com sealed classes para que cada destino saiba exatamente quais eventos de navegação pode emitir.
Quando considerar Navigation 3
O Navigation Compose descrito aqui é a solução estável e é o que a maioria dos apps em produção usa hoje. Existe uma versão mais nova, o Navigation 3, que trata a navegação como estado puro com back stack tipada e integração ainda mais idiomática com Compose. Vale acompanhar — já cobrimos o guia do Navigation 3 com Compose — mas, para novos projetos em 2026 que precisam de estabilidade e material de referência maduro, o Navigation Compose clássico continua sendo a escolha segura.
Conclusão
O Navigation Compose é a fundação de navegação do Android moderno. Dominar NavController, NavHost, rotas type-safe, bottom navigation, grafos aninhados, deep links e savedStateHandle te coloca em posição de arquitetar qualquer fluxo — do app mais simples ao e-commerce com checkout multistep. Comece com rotas serializáveis desde o primeiro dia, mantenha o NavController isolado na UI e deixe que padrões como saveState/restoreState façam o trabalho pesado de preservar a experiência do usuário.
Se quiser ir além, conecte navegação a boas práticas de injeção de dependência com Hilt, layouts de Compose e persistência com DataStore para construir apps Android robustos e mantíveis com Kotlin.