Capítulo 09: Navegação Declarativa com Navigation Compose e Type-Safe Routes
Capítulo 09: Navegação Declarativa com Navigation Compose e Type-Safe Routes
Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose
🗺️ Mapa Conceitual do Grafo de Navegação e Type-Safe Routing
flowchart TD
NAV_HOST["NavHost (Gerenciador do Grafo de Telas)"] --> BACKSTACK["Backstack (Pilha LIFO de Destinos)"]
subgraph ROUTES["Definição de Rotas com Type-Safety (KotlinX Serialization)"]
R_HOME["@Serializable object RotaHome"]
R_DET["@Serializable data class RotaDetalhes(val id: Long, val titulo: String)"]
R_PERFIL["@Serializable object RotaPerfil"]
end
subgraph ACTIONS["Ações do NavHostController"]
NAV_CMD["navController.navigate(RotaDetalhes(id = 42, titulo = 'Android'))"]
POP_CMD["navController.popBackStack()"]
end
NAV_CMD -->|"Empilha Novo Destino"| BACKSTACK
POP_CMD -->|"Desempilha Destino Atual"| BACKSTACK
BACKSTACK -->|"Instancia Composable Alvo"| TARGET["TelaDetalhesScreen(id, titulo)"]
style NAV_HOST fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style BACKSTACK fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style ROUTES fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style TARGET fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
🏛️ 1. A Evolução da Navegação no Android: De Intents a Type-Safe Compose
Historicamente, a navegação entre telas no Android dependia de Intents explícitas para abrir novas Activities pesadas ou transações manuais e frágeis de FragmentTransaction com FragmentManager. A introdução do Navigation Compose unificou toda a arquitetura sob o modelo de Single-Activity Architecture, tratando telas como simples composables empilhadas em um grafo declarativo:
- Fim das Strings Mágicas com Type-Safe Routes: Nas primeiras versões do Navigation Compose, as rotas eram declaradas como strings literais concatenadas (
"detalhes/{id}?origem={origem}"), causando erros frequentes de parsing e exceções de runtime por argumentos ausentes. A partir do Navigation 2.8+, o Google oficializou o suporte a rotas baseadas em classes e objetos anotados com@Serializable, garantindo validação de argumentos estática em tempo de compilação. - Backstack e Single Top Navigation: O
NavHostControllergerencia a pilha de retorno (Backstack) de forma resiliente ao botão “Voltar” nativo do sistema e gestos de borda preditivos do Android 14+. Com o modificadorlaunchSingleTop = true, evitamos a duplicação indesejada de instâncias da mesma tela se o usuário der múltiplos cliques consecutivos em um item de menu. - Deep Linking Transparente: As rotas tipadas mapeiam URLs externas (ex:
https://meuapp.com/detalhes/42) diretamente para as classes serializáveis correspondentes, permitindo que notificações push ou links da web abram diretamente a tela correta com os argumentos preenchidos.
📊 2. Comparativo: Rotas Baseadas em String vs Rotas Type-Safe Modernas
| Dimensão Técnica | Abordagem Legada (Strings / URLs) | Abordagem Moderna (Type-Safe com @Serializable) |
|---|---|---|
| Definição de Rota | composable("produto/{id}") |
composable<RotaProduto> { backStackEntry -> ... } |
| Passagem de Argumentos | navController.navigate("produto/$id") |
navController.navigate(RotaProduto(id = 123)) |
| Resgate de Argumentos | val id = backStackEntry.arguments?.getString("id") |
val args = backStackEntry.toRoute<RotaProduto>() |
| Checagem de Tipos | Nenhuma (falhas silenciosas ou crashes em runtime) | 100% validado pelo compilador Kotlin |
| Renomeação / Refatoração | Manual com alto risco de strings esquecidas | Segura com auto-refactor da IDE (Shift+F6) |
💻 3. Laboratório Prático: Grafo de Navegação Type-Safe com Passagem de Objeto
O exemplo a seguir demonstra a configuração completa de um fluxo de duas telas com rotas estritamente tipadas via Kotlinx Serialization:
package br.com.portal.mobile.android.cap09
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
// 1. Definição Canônica de Rotas Tipadas
@Serializable
object RotaListaProdutos
@Serializable
data class RotaDetalhesProduto(val produtoId: Long, val nomeProduto: String)
// 2. Grafo Principal de Navegação
@Composable
fun AppNavigationHost() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = RotaListaProdutos
) {
// Tela 1: Catálogo de Produtos
composable<RotaListaProdutos> {
CatalogoScreen(
onAbrirDetalhes = { id, nome ->
navController.navigate(RotaDetalhesProduto(produtoId = id, nomeProduto = nome))
}
)
}
// Tela 2: Detalhes com argumentos tipados automáticos
composable<RotaDetalhesProduto> { backStackEntry ->
val detalhes = backStackEntry.toRoute<RotaDetalhesProduto>()
DetalhesScreen(
produtoId = detalhes.produtoId,
nomeProduto = detalhes.nomeProduto,
onVoltar = { navController.popBackStack() }
)
}
}
}
// 3. Telas Composable Desacopladas
@Composable
fun CatalogoScreen(onAbrirDetalhes: (Long, String) -> Unit) {
Scaffold { padding ->
Column(
modifier = Modifier.fillMaxSize().padding(padding).padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Catálogo de Dispositivos", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onAbrirDetalhes(101L, "Smartphone Android 14 Flagship") },
modifier = Modifier.fillMaxWidth()
) {
Text("Ver Detalhes do Smartphone Flagship")
}
}
}
}
@Composable
fun DetalhesScreen(produtoId: Long, nomeProduto: String, onVoltar: () -> Unit) {
Scaffold { padding ->
Column(
modifier = Modifier.fillMaxSize().padding(padding).padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Especificações do Produto", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Text("Código Identificador: #$produtoId", style = MaterialTheme.typography.bodyMedium)
Text("Nome Comercial: $nomeProduto", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(20.dp))
OutlinedButton(onClick = onVoltar, modifier = Modifier.fillMaxWidth()) {
Text("Retornar ao Catálogo")
}
}
}
}
⚠️ 4. Armadilhas Comuns no Navigation Compose
- Passagem Indiscriminada de Entidades Complexas na Rota: Evite passar objetos inteiros ou listas pesadas como argumentos de navegação. A boa prática canônica é passar apenas chaves primitivas (IDs) e permitir que o ViewModel da tela de destino carregue os detalhes completos a partir do repositório/banco de dados local.
- Passar
NavControllerpara Telas Internas: Nunca passe a instância doNavControllerpara subcomponentes de apresentação. Sempre eleve as ações de navegação através de lambdas (onAbrirDetalhes: () -> Unit), preservando a testabilidade das telas. - Múltiplos Cliques Causando Múltiplas Navegações: Ao navegar, utilize blocos com
launchSingleTop = trueou controle de debounce para impedir que um usuário impaciente dê múltiplos toques rápidos e abra 5 instâncias empilhadas da mesma tela.