🌀 P14: Portais Dimensionais (Navigation Compose com Argumentos)
Bem-vindo à terceira etapa da Fase 7: Arquitetura Avançada & Navegação! 🌀
Neste projeto de alto impacto prático, você abandonará de vez a navegação manual baseada em gatilhos de estados booleanos e aprenderá a utilizar a biblioteca oficial de roteamento do Android: o Navigation Compose. Com a temática de viagem interdimensional de Rick e Morty, criaremos um aplicativo onde o usuário navega entre uma lista de mundos paralelos e clica para entrar no portal, passando o identificador exato da dimensão como parâmetro de rota.
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 🎓 Trilha Essencial completa: Projetos P01-P10 (Cap 01-22)
- 🏗️ Projeto anterior: P13: TARDIS Panel
- 📘 Cap 11: Navegação entre Fases (Menus)
🎯 Objetivo do Projeto
Estruturar o fluxo de roteamento de um app multi-telas de design escuro contendo:
- Controller central de Rotas: Declarar e injetar o
NavControllerreativo. - Contêiner de Hosts: Estruturar a árvore de telas com
NavHost. - Passagem de Argumentos: Configurar parâmetros dinâmicos (
dimensaoId) na URL do link da rota de destino. - Navegação e Retorno: Implementar os comandos de navegação
navigate()e retorno de pilhapopBackStack().
graph TD
A[Usuário abre app na rota lista_dimensoes] --> B[LazyColumn exibe mundos colecionados]
B -->|Toca no Card| C[Executa navController.navigate detalhe_dimensao/ID]
C --> D[NavHost intercepta rota detalhe_dimensao/ID]
D --> E[Extrai parametro dimensaoId do argumento]
E --> F[Inicializa TelaDetalheDimensao renderizando mundo específico]
F -->|Toca em Voltar| G[Executa navController.popBackStack]
G --> B
📖 Dicionário do Projeto
- Navigation Compose: O framework oficial desenvolvido pelo Google para gerenciar telas, empilhamentos e históricos de navegação de forma nativa e declarativa dentro do Jetpack Compose.
- NavController: O objeto reativo mestre encarregado de rastrear a pilha de telas ativas e efetuar os comandos de transição (
navigate) ou retorno. - NavHost: O contêiner de layout especial que envelopa a árvore de navegação, associando caminhos de texto (rotas) aos componentes de telas reais (
composable). - navArgument: Função auxiliar usada para configurar regras, formatos e tipagens (como
NavType.StringType) de argumentos dinâmicos embutidos nas rotas. - popBackStack(): Comando que destrói a tela ativa no topo e retorna o usuário de forma segura à tela anterior na pilha de histórico.
🛠️ Passo 1: Configurando as Dependências de Navegação (Gradle)
- Abra o arquivo
build.gradle (Module :app)e adicione a biblioteca de navegação oficial do Compose:dependencies { // Navigation Compose implementation "androidx.navigation:navigation-compose:2.7.5" ... }Clique em Sync Now.
🛠️ Passo 2: Declarando as Rotas no NavHost
No seu arquivo MainActivity.kt, inicialize o controlador de navegação e declare os caminhos lógicos de texto das suas telas:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "lista_dimensoes") {
// Rota Inicial (Estática)
composable("lista_dimensoes") {
TelaListaDimensoes(onDimensaoClick = { id ->
navController.navigate("detalhe_dimensao/$id")
})
}
// Rota de Destino (Dinâmica com Argumento)
composable(
route = "detalhe_dimensao/{dimensaoId}",
arguments = listOf(navArgument("dimensaoId") { type = NavType.StringType })
) { backStackEntry ->
val dimensaoId = backStackEntry.arguments?.getString("dimensaoId") ?: ""
TelaDetalheDimensao(dimensaoId = dimensaoId, onVoltarClick = {
navController.popBackStack()
})
}
}
🧠 Passo 3: Passando e Lendo Parâmetros nas Telas
Ao clicar no Card na primeira tela, disparamos a rota injetando a String do identificador do item: onDimensaoClick = { id -> navController.navigate("detalhe_dimensao/$id") }.
A segunda tela recebe o parâmetro dimensaoId, filtra a lista estática e exibe as informações específicas do portal clicado com total segurança arquitetural.
🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
- Transição Animada: utilize
AnimatedContentou transições de entrada/saída doNavHostpara animar a troca entre as rotas. - Deep Link: configure um
NavDeepLinkpara abrir diretamente a tela de detalhe de uma dimensão específica.
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos Kotlin completos abaixo para validar sua implementação.
📄 Código de Lógica e Tela Completa (MainActivity.kt)
Disponível em: app/src/main/java/br/com/curso/portais/MainActivity.kt
package br.com.curso.portais
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PortaisTheme {
PortaisAppNavigation()
}
}
}
}
@Composable
fun PortaisTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFF00E676), // Verde Portal
background = Color(0xFF0C0F12),
surface = Color(0xFF1E232A)
),
content = content
)
}
// 1. DATA MODEL
data class Dimensao(val id: String, val name: String, val dangerLevel: String, val description: String)
val dimensoesList = listOf(
Dimensao("C137", "Terra C-137", "ALTO", "Dimensão natal de Rick Sanchez C-137. Totalmente devastada por Cronenbergs."),
Dimensao("J197", "Dimensão Doce", "BAIXO", "Onde tudo é feito de doces, pirulitos e chocolate. Altamente segura."),
Dimensao("F345", "Dimensão dos Sofás", "MÉDIO", "Onde as pessoas são sofás e os sofás são pessoas. Cuidado ao sentar.")
)
@Composable
fun PortaisAppNavigation() {
// 2. NAV CONTROLLER
val navController = rememberNavController()
// 3. NAV HOST COM COMPOSABLE ROUTES
NavHost(navController = navController, startDestination = "lista_dimensoes") {
composable("lista_dimensoes") {
TelaListaDimensoes(
onDimensaoClick = { id ->
navController.navigate("detalhe_dimensao/$id")
}
)
}
composable(
route = "detalhe_dimensao/{dimensaoId}",
arguments = listOf(navArgument("dimensaoId") { type = NavType.StringType })
) { backStackEntry ->
val dimensaoId = backStackEntry.arguments?.getString("dimensaoId") ?: ""
TelaDetalheDimensao(
dimensaoId = dimensaoId,
onVoltarClick = {
navController.popBackStack()
}
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaListaDimensoes(onDimensaoClick: (String) -> Unit) {
Scaffold(
topBar = {
SmallTopAppBar(
title = { Text("PORTAL INTERDIMENSIONAL", fontWeight = FontWeight.Black, letterSpacing = 2.sp) },
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color(0xFF1E232A))
)
}
) { padding ->
LazyColumn(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.background(Color(0xFF0C0F12))
.padding(16.dp)
) {
items(dimensoesList) { dimensao ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.clickable { onDimensaoClick(dimensao.id) },
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E232A)),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(dimensao.name, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 16.sp)
Text("ID: ${dimensao.id}", color = Color.Gray, fontSize = 12.sp)
}
Text("ENTRAR 🌀", color = Color(0xFF00E676), fontWeight = FontWeight.Bold)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaDetalheDimensao(dimensaoId: String, onVoltarClick: () -> Unit) {
val dimensao = dimensoesList.firstOrNull { it.id == dimensaoId }
Scaffold(
topBar = {
SmallTopAppBar(
title = { Text("MUNDO COLETADO", fontWeight = FontWeight.Bold) },
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color(0xFF1E232A))
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.background(Color(0xFF0C0F12))
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (dimensao != null) {
Box(
modifier = Modifier
.size(120.dp)
.background(Color(0xFF00E676).copy(alpha = 0.1f), shape = RoundedCornerShape(60.dp)),
contentAlignment = Alignment.Center
) {
Text("🌀", fontSize = 60.sp)
}
Spacer(modifier = Modifier.height(24.dp))
Text(dimensao.name, fontSize = 28.sp, fontWeight = FontWeight.Black, color = Color.White)
Text("CÓDIGO DIMENSIONAL: ${dimensao.id}", color = Color.Gray, fontSize = 14.sp)
Spacer(modifier = Modifier.height(16.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E232A)),
shape = RoundedCornerShape(16.dp)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text("Perigo: ${dimensao.dangerLevel}", color = if (dimensao.dangerLevel == "ALTO") Color.Red else Color.Green, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Text(dimensao.description, color = Color.White.copy(alpha = 0.8f))
}
}
} else {
Text("Dimensão Não Encontrada", color = Color.Red)
}
Spacer(modifier = Modifier.height(32.dp))
Button(onClick = onVoltarClick, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E232A))) {
Text("VOLTAR AO MULTIVERSO", color = Color.White)
}
}
}
}