🌀 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:


🎯 Objetivo do Projeto

Estruturar o fluxo de roteamento de um app multi-telas de design escuro contendo:

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


🛠️ Passo 1: Configurando as Dependências de Navegação (Gradle)

  1. 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:


📖 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)
            }
        }
    }
}