📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.

🏰 P11: Guilda dos Heróis (Navigation Compose & Biometria)

Bem-vindo à ponte entre o Núcleo Essencial e a Trilha Bônus! 🏰

Até aqui, seus aplicativos trocavam de “tela” apenas alternando o que era desenhado na mesma Activity com variáveis de estado (when (telaAtual) { ... }). Isso funciona para casos simples, mas não escala: não existe histórico de navegação (o botão físico/gestual de “voltar” do smartphone não funciona), não há passagem de parâmetros tipada via rota e a lógica de roteamento fica misturada com a UI.

Neste projeto prático, você aprenderá a arquitetura padrão da indústria com o Navigation Compose oficial do Google e o hardware de segurança com o BiometricPrompt (autenticação por digital e reconhecimento facial)!

Desenvolveremos a Guilda dos Heróis, um aplicativo multi-telas de tema medieval escuro e dourado com 5 rotas completas: catálogo de heróis, ficha detalhada com argumento tipado na URL (detalhe_heroi/{heroiId}), tela sobre, autenticação biométrica e o cofre secreto dos tesouros ancestrais!

Fluxo Completo de Telas da Guilda dos Heróis


📱 Galeria de Telas da Aplicação

1. Lista de Heróis 2. Ficha do Herói 3. Autenticação BiometricPrompt 4. Sala dos Tesouros
Tela 1 - Lista Tela 2 - Detalhe Tela 3 - Biometria Tela 4 - Tesouros
Lista com banner para o cofre e atalho sobre. Ficha recebendo id via argumento de rota. Diálogo nativo do Android com sensor de digital. Área secreta liberada após autenticação biométrica.

✅ Pré-requisitos e Continuidade

Antes de iniciar este laboratório, certifique-se de ter compreendido:


🎯 Objetivos de Aprendizagem

Ao concluir este projeto autoguiado, você será capaz de:

  1. Configurar o Navigation Compose: Instanciar o rememberNavController() e mapear rotas declarativas centralizadas dentro do NavHost.
  2. Passar Argumentos Tipados via Rota: Enviar o id do herói através da URL de navegação ("detalhe_heroi/{heroiId}") validado com navArgument e NavType.IntType.
  3. Gerenciar a Pilha de Retrocesso (Backstack): Utilizar navController.navigate() para empilhar telas e navController.popBackStack() para desempilhar, integrando o botão físico de voltar do Android.
  4. Implementar Navegação Não-Linear: Conectar rotas independentes (como a tela “Sobre” e o “Cofre Secreto”) a partir de qualquer ponto da aplicação.
  5. Integrar Biometria Nativa (BiometricPrompt): Validar a identidade do usuário com sensor de digital ou reconhecimento facial antes de liberar áreas protegidas do app.

🏗️ Arquitetura e Grafo de Navegação

graph TD
    A[MainActivity: FragmentActivity] --> B[rememberNavController]
    B --> C[NavHost: Grafo Central de 5 Rotas]
    
    C --> D[Rota: lista_herois - Tela Inicial]
    
    D -->|Clique no Card do Herói ID| E["Rota: detalhe_heroi/{heroiId}"]
    D -->|Clique no Botão SOBRE| F[Rota: sobre_guilda]
    D -->|Clique no Card COFRE DOS MESTRES| G[Rota: cofre_biometria]
    
    E -->|TopAppBar seta voltar: popBackStack| D
    F -->|TopAppBar seta voltar: popBackStack| D
    
    G --> H{Sensor Biométrico do Android}
    H -->|Digital / Rosto Validado com Sucesso| I[Rota: sala_dos_tesouros]
    H -->|Erro ou Cancelado| G
    
    I -->|TopAppBar seta voltar: popBackStack| G

📖 Dicionário Técnico do Projeto

Termo / Componente O que é e para que serve?
Navigation Compose Biblioteca oficial do Google (androidx.navigation:navigation-compose) que substitui a troca de telas manual por um sistema profissional de rotas e pilha de histórico.
NavController O controlador de navegação. Objeto responsável por rastrear a tela ativa e gerenciar a pilha de retrocesso (backstack).
NavHost O contêiner declarativo que mapeia cada rota (composable("nome_da_rota") { ... }) com seu respectivo componente visual.
navArgument / NavType Construtor de validação de argumentos que extrai parâmetros de rotas e converte para tipos seguros (IntType, StringType, BooleanType).
popBackStack() Método que remove a tela atual do topo da pilha de navegação e retorna o usuário para a tela anterior exata de onde ele veio.
BiometricPrompt API unificada do AndroidX que renderiza o diálogo nativo do sistema operacional para leitura de digital ou reconhecimento facial.
FragmentActivity Classe base do AndroidX que fornece o gerenciador de fragmentos necessário para a exibição de diálogos do BiometricPrompt.

🛠️ Passo a Passo de Implementação

🚀 Passo 1: Criando o Novo Projeto no Android Studio

  1. Abra o Android Studio e clique em New Project (ou File > New > New Project).
  2. Selecione o template Empty Activity (com o ícone do Jetpack Compose 🌌).
  3. Preencha as configurações do projeto:
    • Name: Guilda dos Heróis (ou android_p11_herois_biometria)
    • Package name: br.com.curso.guildaherois
    • Save location: Pasta do seu projeto no repositório
    • Language: Kotlin
    • Minimum SDK: API 24 ("Nougat"; Android 7.0) ou superior
    • Build configuration language: Groovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)
  4. Clique em Finish e aguarde a indexação do Gradle.

📦 Passo 2: Adicionando as Dependências no Gradle (build.gradle)

Abra app > build.gradle e adicione o Navigation Compose, o Biometric e o Fragment KTX:

dependencies {
    implementation platform('androidx.compose:compose-bom:2024.02.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    implementation 'androidx.activity:activity-compose:1.8.2'
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'

    // Navigation Compose (navegação profissional com rotas e backstack)
    implementation 'androidx.navigation:navigation-compose:2.7.6'

    // Biometria e Segurança (BiometricPrompt)
    implementation 'androidx.biometric:biometric:1.1.0'
    implementation 'androidx.fragment:fragment-ktx:1.6.2'
}

Clique em Sync Now.


📄 Passo 3: Declarando a Permissão no Manifesto (AndroidManifest.xml)

Abra app > src > main > AndroidManifest.xml e declare a permissão de biometria:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Permissão para leitura biométrica no Cofre da Guilda -->
    <uses-permission android:name="android.permission.USE_BIOMETRIC" />

    <application
        android:allowBackup="true"
        android:label="Guilda dos Heróis"
        android:supportsRtl="true"
        android:theme="@style/Theme.GuildaHerois">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

🎴 Passo 4: Modelo de Dados dos Heróis (Heroi)

No topo de MainActivity.kt, declaramos a classe de heróis com seus atributos e a lista inicial da guilda:

data class Heroi(
    val id: Int,
    val nome: String,
    val classe: String,
    val nivel: Int,
    val poder: String,
    val emoji: String,
    val descricao: String = "Um destemido guardião pronto para defender o reino contra qualquer ameaça."
)

val listaHerois = listOf(
    Heroi(1, "Thoran, o Guerreiro", "Guerreiro", 12, "Golpe Sísmico", "⚔️", "Especialista em combate corpo a corpo e escudos impenetráveis."),
    Heroi(2, "Lyra Sombraverde", "Arqueira", 9, "Chuva de Flechas", "🏹", "Atiradora de elite capaz de atingir alvos a centenas de metros nas sombras da floresta."),
    Heroi(3, "Magnus, o Arcano", "Mago", 15, "Bola de Fogo Cósmica", "🔥", "Mestre dos elementos arcanos e conjurações de alta destruição."),
    Heroi(4, "Kira, a Curandeira", "Clériga", 10, "Luz Sagrada", "✨", "Guardiã da bênção divina com poder de restauração total de pontos de vida.")
)

🎨 Passo 5: Tema Medieval Dourado (GuildaTheme)

@Composable
fun GuildaTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = darkColorScheme(
            primary = Color(0xFFFFB300),    // Dourado Nobre
            secondary = Color(0xFF7B1FA2),  // Roxo Arcano
            tertiary = Color(0xFF00E676),   // Verde Destaque
            background = Color(0xFF120B1E), // Roxo Noturno Profundo
            surface = Color(0xFF1F1430),
            surfaceVariant = Color(0xFF2A1B40)
        ),
        content = content
    )
}

🧭 Passo 6: O Grafo Mestre de Navegação (NavHost)

Declaramos todas as 5 rotas conectadas em um único ponto central:

@Composable
fun GuildaAppNavigation(activity: FragmentActivity) {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "lista_herois") {
        // Rota 1: Lista de Heróis (Tela Inicial)
        composable("lista_herois") {
            TelaListaHerois(
                onHeroiClick = { id -> navController.navigate("detalhe_heroi/$id") },
                onSobreClick = { navController.navigate("sobre_guilda") },
                onCofreClick = { navController.navigate("cofre_biometria") }
            )
        }

        // Rota 2: Ficha com Argumento Tipado {heroiId}
        composable(
            route = "detalhe_heroi/{heroiId}",
            arguments = listOf(navArgument("heroiId") { type = NavType.IntType })
        ) { backStackEntry ->
            val heroiId = backStackEntry.arguments?.getInt("heroiId") ?: -1
            TelaDetalheHeroi(
                heroiId = heroiId,
                onVoltarClick = { navController.popBackStack() }
            )
        }

        // Rota 3: Sobre a Guilda
        composable("sobre_guilda") {
            TelaSobreGuilda(onVoltarClick = { navController.popBackStack() })
        }

        // Rota 4: Cofre com Biometria
        composable("cofre_biometria") {
            TelaCofreBiometria(
                activity = activity,
                onAutenticadoComSucesso = { navController.navigate("sala_dos_tesouros") },
                onVoltarClick = { navController.popBackStack() }
            )
        }

        // Rota 5: Sala dos Tesouros Desbloqueada
        composable("sala_dos_tesouros") {
            TelaSalaDosTesouros(onVoltarClick = { navController.popBackStack() })
        }
    }
}

📋 Passo 7: Tela 1 - Lista de Heróis e Banner do Cofre (TelaListaHerois)

A tela de entrada lista os guerreiros disponíveis e inclui o card dourado de acesso ao Cofre Secreto com Biometria:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaListaHerois(onHeroiClick: (Int) -> Unit, onSobreClick: () -> Unit, onCofreClick: () -> Unit) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("GUILDA DOS HERÓIS", fontWeight = FontWeight.Black, letterSpacing = 2.sp) },
                actions = {
                    TextButton(onClick = onSobreClick) {
                        Text("SOBRE", color = Color(0xFFFFB300), fontWeight = FontWeight.Bold)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF1F1430), titleContentColor = Color.White)
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        LazyColumn(modifier = Modifier.padding(padding).fillMaxSize().padding(horizontal = 16.dp)) {
            item {
                // Card de Acesso ao Cofre com Biometria
                Card(
                    modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).clickable { onCofreClick() },
                    shape = RoundedCornerShape(18.dp),
                    colors = CardDefaults.cardColors(containerColor = Color(0xFF2A1538))
                ) {
                    Row(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
                        Box(modifier = Modifier.size(46.dp).clip(CircleShape).background(Color(0xFFFFB300).copy(alpha = 0.2f)), contentAlignment = Alignment.Center) {
                            Text("🔐", fontSize = 22.sp)
                        }
                        Spacer(modifier = Modifier.width(14.dp))
                        Column(modifier = Modifier.weight(1f)) {
                            Text("COFRE DOS MESTRES", fontWeight = FontWeight.Black, color = Color(0xFFFFB300), fontSize = 14.sp)
                            Text("Acesso restrito via Biometria / Digital", color = Color.LightGray, fontSize = 11.sp)
                        }
                        Text("ENTRAR ›", color = Color(0xFFFFB300), fontSize = 12.sp, fontWeight = FontWeight.Bold)
                    }
                }
            }

            items(listaHerois, key = { it.id }) { heroi ->
                Card(
                    modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp).clickable { onHeroiClick(heroi.id) },
                    colors = CardDefaults.cardColors(containerColor = Color(0xFF1F1430)),
                    shape = RoundedCornerShape(16.dp)
                ) {
                    Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
                        Text(heroi.emoji, fontSize = 32.sp)
                        Spacer(modifier = Modifier.width(16.dp))
                        Column(modifier = Modifier.weight(1f)) {
                            Text(heroi.nome, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 16.sp)
                            Text("${heroi.classe} • Nível ${heroi.nivel}", color = Color.Gray, fontSize = 13.sp)
                        }
                        Text("›", color = Color(0xFFFFB300), fontSize = 24.sp, fontWeight = FontWeight.Bold)
                    }
                }
            }
        }
    }
}

Lista de Heróis


🛡️ Passo 8: Tela 2 - Ficha do Herói com popBackStack() (TelaDetalheHeroi)

A tela de detalhes busca o herói a partir do heroiId extraído da URL da rota e implementa o botão voltar nativo:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaDetalheHeroi(heroiId: Int, onVoltarClick: () -> Unit) {
    val heroi = listaHerois.firstOrNull { it.id == heroiId }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("FICHA DO HERÓI", fontWeight = FontWeight.Bold) },
                navigationIcon = {
                    IconButton(onClick = onVoltarClick) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Voltar", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF1F1430), titleContentColor = Color.White)
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        Column(modifier = Modifier.padding(padding).fillMaxSize().padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
            if (heroi != null) {
                Box(modifier = Modifier.size(110.dp).clip(CircleShape).background(Color(0xFF1F1430)), contentAlignment = Alignment.Center) {
                    Text(heroi.emoji, fontSize = 64.sp)
                }
                Spacer(modifier = Modifier.height(16.dp))
                Text(heroi.nome, fontSize = 22.sp, fontWeight = FontWeight.Black, color = Color.White)
                Text(heroi.classe.uppercase(), color = Color(0xFFFFB300), fontWeight = FontWeight.Bold, fontSize = 13.sp)

                Spacer(modifier = Modifier.height(24.dp))

                Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color(0xFF1F1430)), shape = RoundedCornerShape(16.dp)) {
                    Column(modifier = Modifier.padding(20.dp)) {
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text("Nível de Experiência:", color = Color.Gray)
                            Text("Nível ${heroi.nivel}", fontWeight = FontWeight.Bold, color = Color(0xFFFFB300))
                        }
                        Spacer(modifier = Modifier.height(10.dp))
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text("Habilidade Especial:", color = Color.Gray)
                            Text(heroi.poder, fontWeight = FontWeight.Bold, color = Color.White)
                        }
                        Spacer(modifier = Modifier.height(14.dp))
                        Text(heroi.descricao, color = Color.White.copy(alpha = 0.85f), fontSize = 13.sp)
                    }
                }
            }
        }
    }
}

Ficha do Herói


🔐 Passo 9: Tela 4 - Cofre com Autenticação Biométrica (BiometricPrompt)

Ao tocar no botão de autenticação, o aplicativo dispara o diálogo nativo do leitor biométrico do Android:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaCofreBiometria(
    activity: FragmentActivity,
    onAutenticadoComSucesso: () -> Unit,
    onVoltarClick: () -> Unit
) {
    val context = LocalContext.current
    val executor = remember { ContextCompat.getMainExecutor(context) }

    fun dispararBiometria() {
        val biometricManager = BiometricManager.from(context)
        val canAuthenticate = biometricManager.canAuthenticate(
            BiometricManager.Authenticators.BIOMETRIC_STRONG or
            BiometricManager.Authenticators.BIOMETRIC_WEAK or
            BiometricManager.Authenticators.DEVICE_CREDENTIAL
        )

        if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
            val biometricPrompt = BiometricPrompt(
                activity,
                executor,
                object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                        super.onAuthenticationSucceeded(result)
                        Toast.makeText(context, "Acesso Concedido pelo Guardião!", Toast.LENGTH_SHORT).show()
                        onAutenticadoComSucesso()
                    }

                    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                        super.onAuthenticationError(errorCode, errString)
                        Toast.makeText(context, "Biometria Cancelada ou Indisponível", Toast.LENGTH_SHORT).show()
                    }
                }
            )

            val promptInfo = BiometricPrompt.PromptInfo.Builder()
                .setTitle("Cofre dos Mestres da Guilda")
                .setSubtitle("Autentique com sua digital ou reconhecimento facial")
                .setAllowedAuthenticators(
                    BiometricManager.Authenticators.BIOMETRIC_STRONG or
                    BiometricManager.Authenticators.BIOMETRIC_WEAK or
                    BiometricManager.Authenticators.DEVICE_CREDENTIAL
                )
                .build()

            biometricPrompt.authenticate(promptInfo)
        } else {
            // Fallback de desenvolvimento para emuladores
            Toast.makeText(context, "Sensor biométrico simulado para desenvolvimento!", Toast.LENGTH_SHORT).show()
            onAutenticadoComSucesso()
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("AUTENTICAÇÃO DO COFRE", fontWeight = FontWeight.Bold) },
                navigationIcon = {
                    IconButton(onClick = onVoltarClick) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Voltar", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF1F1430), titleContentColor = Color.White)
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        Column(
            modifier = Modifier.padding(padding).fillMaxSize().padding(32.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Box(modifier = Modifier.size(90.dp).clip(CircleShape).background(Color(0xFFFFB300).copy(alpha = 0.15f)), contentAlignment = Alignment.Center) {
                Text("🔐", fontSize = 42.sp)
            }
            Spacer(modifier = Modifier.height(24.dp))
            Text("ÁREA RESTRITA DA GUILDA", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color.White)
            Spacer(modifier = Modifier.height(36.dp))
            Button(
                onClick = { dispararBiometria() },
                modifier = Modifier.fillMaxWidth().height(52.dp),
                shape = RoundedCornerShape(12.dp),
                colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFFB300))
            ) {
                Text("AUTENTICAR COM BIOMETRIA", fontWeight = FontWeight.Black, color = Color.Black)
            }
        }
    }
}

Diálogo de Biometria Nativo


👑 Passo 10: Tela 5 - Sala dos Tesouros Desbloqueada (TelaSalaDosTesouros)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaSalaDosTesouros(onVoltarClick: () -> Unit) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("👑 SALA DOS TESOUROS", fontWeight = FontWeight.Black, color = Color(0xFFFFB300)) },
                navigationIcon = {
                    IconButton(onClick = onVoltarClick) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Voltar", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF1F1430))
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        Column(modifier = Modifier.padding(padding).fillMaxSize().padding(20.dp)) {
            Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color(0xFF2A1538)), shape = RoundedCornerShape(16.dp)) {
                Column(modifier = Modifier.padding(20.dp)) {
                    Text("💎 ARTEFATOS ANCESTRAIS DESBLOQUEADOS", fontWeight = FontWeight.Black, color = Color(0xFFFFB300), fontSize = 14.sp)
                    Spacer(modifier = Modifier.height(12.dp))
                    Text("• 🗡️ Espada da Fênix (Ataque +120)", color = Color.White)
                    Text("• 🛡️ Égide dos Titãs (Defesa +95)", color = Color.White)
                    Text("• 🔮 Orbe dos Elementos Primordiais", color = Color.White)
                }
            }
        }
    }
}

Sala dos Tesouros Desbloqueada


🔍 Guia de Diagnóstico & Resolução de Problemas (Troubleshooting)

Sintoma Observado Causa Provável Como Resolver
IllegalArgumentException: Navigation destination that matches request cannot be found A rota chamada em navController.navigate("...") não foi registrada dentro do NavHost. Verifique se a string digitada coincide exatamente com a rota declarada no composable("...").
O argumento chega como null ou -1 na tela de detalhes O nome do argumento na rota (ex: {heroiId}) é diferente do nome usado no navArgument ou no getInt(...). Certifique-se de que a chave {heroiId} seja idêntica em todos os três lugares: rota, navArgument("heroiId") e getInt("heroiId").
Ao clicar no botão voltar físico, o app não fecha e fica em loop Uso indevido de navigate("lista_herois") para voltar, em vez de popBackStack(). Sempre utilize navController.popBackStack() para desempilhar a tela atual.
BiometricPrompt requires a FragmentActivity A MainActivity está estendendo ComponentActivity em vez de FragmentActivity. Altere a assinatura da classe para class MainActivity : FragmentActivity().
No Emulador, o BiometricPrompt diz que não há biometria cadastrada O emulador não tem digital cadastrada nas configurações do Android. Vá em Settings > Security > Fingerprint no Emulador e adicione uma digital pelo painel lateral (Extended Controls > Fingerprint).

🏆 Desafios e Upgrades (Mão na Massa!)

  1. 🔄 Transições Animadas de Rota: Utilize as extensões enterTransition e exitTransition no composable para criar transições cinematográficas de slide horizontal entre as telas da guilda.
  2. ⚔️ Tela de Edição de Nível: Crie uma sexta rota "editar_heroi/{heroiId}" com um formulário para aumentar o nível do guerreiro e salvar em uma lista mutável reativa (mutableStateListOf).
  3. 📜 Filtro de Classes: Adicione abas na lista de heróis para filtrar por Guerreiros, Magos e Arqueiros.

📖 Gabarito Oficial de Código (Para Conferência)

Disponível em: app/src/main/java/br/com/curso/guildaherois/MainActivity.kt

package br.com.curso.guildaherois

import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument

data class Heroi(
    val id: Int,
    val nome: String,
    val classe: String,
    val nivel: Int,
    val poder: String,
    val emoji: String,
    val descricao: String = "Um destemido guardião pronto para defender o reino contra qualquer ameaça."
)

val listaHerois = listOf(
    Heroi(1, "Thoran, o Guerreiro", "Guerreiro", 12, "Golpe Sísmico", "⚔️", "Especialista em combate corpo a corpo e escudos impenetráveis."),
    Heroi(2, "Lyra Sombraverde", "Arqueira", 9, "Chuva de Flechas", "🏹", "Atiradora de elite capaz de atingir alvos a centenas de metros nas sombras da floresta."),
    Heroi(3, "Magnus, o Arcano", "Mago", 15, "Bola de Fogo Cósmica", "🔥", "Mestre dos elementos arcanos e conjurações de alta destruição."),
    Heroi(4, "Kira, a Curandeira", "Clériga", 10, "Luz Sagrada", "✨", "Guardiã da bênção divina com poder de restauração total de pontos de vida.")
)

class MainActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            GuildaTheme {
                GuildaAppNavigation(this)
            }
        }
    }
}

@Composable
fun GuildaTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = darkColorScheme(
            primary = Color(0xFFFFB300),
            secondary = Color(0xFF7B1FA2),
            tertiary = Color(0xFF00E676),
            background = Color(0xFF120B1E),
            surface = Color(0xFF1F1430),
            surfaceVariant = Color(0xFF2A1B40)
        ),
        content = content
    )
}

@Composable
fun GuildaAppNavigation(activity: FragmentActivity) {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "lista_herois") {
        composable("lista_herois") {
            TelaListaHerois(
                onHeroiClick = { id -> navController.navigate("detalhe_heroi/$id") },
                onSobreClick = { navController.navigate("sobre_guilda") },
                onCofreClick = { navController.navigate("cofre_biometria") }
            )
        }

        composable(
            route = "detalhe_heroi/{heroiId}",
            arguments = listOf(navArgument("heroiId") { type = NavType.IntType })
        ) { backStackEntry ->
            val heroiId = backStackEntry.arguments?.getInt("heroiId") ?: -1
            TelaDetalheHeroi(
                heroiId = heroiId,
                onVoltarClick = { navController.popBackStack() }
            )
        }

        composable("sobre_guilda") {
            TelaSobreGuilda(onVoltarClick = { navController.popBackStack() })
        }

        composable("cofre_biometria") {
            TelaCofreBiometria(
                activity = activity,
                onAutenticadoComSucesso = { navController.navigate("sala_dos_tesouros") },
                onVoltarClick = { navController.popBackStack() }
            )
        }

        composable("sala_dos_tesouros") {
            TelaSalaDosTesouros(onVoltarClick = { navController.popBackStack() })
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaListaHerois(
    onHeroiClick: (Int) -> Unit,
    onSobreClick: () -> Unit,
    onCofreClick: () -> Unit
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        "GUILDA DOS HERÓIS",
                        fontWeight = FontWeight.Black,
                        letterSpacing = 2.sp,
                        fontSize = 18.sp
                    )
                },
                actions = {
                    TextButton(onClick = onSobreClick) {
                        Text("SOBRE", color = Color(0xFFFFB300), fontWeight = FontWeight.Bold)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = Color(0xFF1F1430),
                    titleContentColor = Color.White
                )
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        LazyColumn(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize()
                .padding(horizontal = 16.dp)
        ) {
            item {
                Spacer(modifier = Modifier.height(8.dp))

                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 8.dp)
                        .clickable { onCofreClick() },
                    shape = RoundedCornerShape(18.dp),
                    colors = CardDefaults.cardColors(containerColor = Color(0xFF2A1538))
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(
                                Brush.horizontalGradient(
                                    listOf(Color(0xFF4A148C).copy(alpha = 0.6f), Color(0xFFFFB300).copy(alpha = 0.2f))
                                )
                            )
                            .padding(16.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Box(
                            modifier = Modifier
                                .size(46.dp)
                                .clip(CircleShape)
                                .background(Color(0xFFFFB300).copy(alpha = 0.2f)),
                            contentAlignment = Alignment.Center
                        ) {
                            Text("🔐", fontSize = 22.sp)
                        }
                        Spacer(modifier = Modifier.width(14.dp))
                        Column(modifier = Modifier.weight(1f)) {
                            Text(
                                "COFRE DOS MESTRES",
                                fontWeight = FontWeight.Black,
                                color = Color(0xFFFFB300),
                                fontSize = 14.sp
                            )
                            Text(
                                "Acesso restrito via Biometria / Digital",
                                color = Color.LightGray,
                                fontSize = 11.sp
                            )
                        }
                        Text(
                            "ENTRAR ›",
                            color = Color(0xFFFFB300),
                            fontSize = 12.sp,
                            fontWeight = FontWeight.Bold
                        )
                    }
                }

                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    "HERÓIS DISPONÍVEIS",
                    fontSize = 12.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.Gray,
                    modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)
                )
            }

            items(listaHerois, key = { it.id }) { heroi ->
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 6.dp)
                        .clickable { onHeroiClick(heroi.id) },
                    colors = CardDefaults.cardColors(containerColor = Color(0xFF1F1430)),
                    shape = RoundedCornerShape(16.dp)
                ) {
                    Row(
                        modifier = Modifier.padding(16.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Text(heroi.emoji, fontSize = 32.sp)
                        Spacer(modifier = Modifier.width(16.dp))
                        Column(modifier = Modifier.weight(1f)) {
                            Text(heroi.nome, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 16.sp)
                            Text("${heroi.classe} • Nível ${heroi.nivel}", color = Color.Gray, fontSize = 13.sp)
                        }
                        Text("›", color = Color(0xFFFFB300), fontSize = 24.sp, fontWeight = FontWeight.Bold)
                    }
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaDetalheHeroi(heroiId: Int, onVoltarClick: () -> Unit) {
    val heroi = listaHerois.firstOrNull { it.id == heroiId }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("FICHA DO HERÓI", fontWeight = FontWeight.Bold) },
                navigationIcon = {
                    IconButton(onClick = onVoltarClick) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Voltar", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = Color(0xFF1F1430),
                    titleContentColor = Color.White
                )
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize()
                .padding(24.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            if (heroi != null) {
                Box(
                    modifier = Modifier
                        .size(110.dp)
                        .clip(CircleShape)
                        .background(Color(0xFF1F1430)),
                    contentAlignment = Alignment.Center
                ) {
                    Text(heroi.emoji, fontSize = 64.sp)
                }

                Spacer(modifier = Modifier.height(16.dp))
                Text(heroi.nome, fontSize = 22.sp, fontWeight = FontWeight.Black, color = Color.White)
                Text(heroi.classe.uppercase(), color = Color(0xFFFFB300), fontWeight = FontWeight.Bold, fontSize = 13.sp)

                Spacer(modifier = Modifier.height(24.dp))

                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(containerColor = Color(0xFF1F1430)),
                    shape = RoundedCornerShape(16.dp)
                ) {
                    Column(modifier = Modifier.padding(20.dp)) {
                        Row(
                            modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.SpaceBetween
                        ) {
                            Text("Nível de Experiência:", color = Color.Gray)
                            Text("Nível ${heroi.nivel}", fontWeight = FontWeight.Bold, color = Color(0xFFFFB300))
                        }
                        Spacer(modifier = Modifier.height(10.dp))
                        Row(
                            modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.SpaceBetween
                        ) {
                            Text("Habilidade Especial:", color = Color.Gray)
                            Text(heroi.poder, fontWeight = FontWeight.Bold, color = Color.White)
                        }
                        Spacer(modifier = Modifier.height(14.dp))
                        Divider(color = Color(0xFF2A1B40))
                        Spacer(modifier = Modifier.height(14.dp))
                        Text(
                            text = heroi.descricao,
                            color = Color.White.copy(alpha = 0.85f),
                            fontSize = 13.sp,
                            lineHeight = 20.sp
                        )
                    }
                }
            } else {
                Text("Herói não encontrado!", color = Color.Red, fontSize = 16.sp)
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaSobreGuilda(onVoltarClick: () -> Unit) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("SOBRE A GUILDA", fontWeight = FontWeight.Bold) },
                navigationIcon = {
                    IconButton(onClick = onVoltarClick) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Voltar", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = Color(0xFF1F1430),
                    titleContentColor = Color.White
                )
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize()
                .padding(24.dp)
        ) {
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(containerColor = Color(0xFF1F1430)),
                shape = RoundedCornerShape(16.dp)
            ) {
                Column(modifier = Modifier.padding(20.dp)) {
                    Text(
                        "🏰 A Fortaleza dos Aventureiros",
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Black,
                        color = Color(0xFFFFB300)
                    )
                    Spacer(modifier = Modifier.height(10.dp))
                    Text(
                        "A Guilda dos Heróis foi forjada para treinar e reunir os campeões mais destemidos do continente. Cada herói possui habilidades únicas e acesso a missões lendárias.",
                        color = Color.White.copy(alpha = 0.9f),
                        fontSize = 14.sp,
                        lineHeight = 22.sp
                    )
                    Spacer(modifier = Modifier.height(16.dp))
                    Text("Total de Guardiões Ativos: ${listaHerois.size}", color = Color.Gray, fontSize = 12.sp)
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaCofreBiometria(
    activity: FragmentActivity,
    onAutenticadoComSucesso: () -> Unit,
    onVoltarClick: () -> Unit
) {
    val context = LocalContext.current
    val executor = remember { ContextCompat.getMainExecutor(context) }

    fun dispararBiometria() {
        val biometricManager = BiometricManager.from(context)
        val canAuthenticate = biometricManager.canAuthenticate(
            BiometricManager.Authenticators.BIOMETRIC_STRONG or
                    BiometricManager.Authenticators.BIOMETRIC_WEAK or
                    BiometricManager.Authenticators.DEVICE_CREDENTIAL
        )

        if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
            val biometricPrompt = BiometricPrompt(
                activity,
                executor,
                object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                        super.onAuthenticationSucceeded(result)
                        Toast.makeText(context, "Acesso Concedido pelo Guardião!", Toast.LENGTH_SHORT).show()
                        onAutenticadoComSucesso()
                    }

                    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                        super.onAuthenticationError(errorCode, errString)
                        Toast.makeText(context, "Biometria Cancelada ou Indisponível", Toast.LENGTH_SHORT).show()
                    }
                }
            )

            val promptInfo = BiometricPrompt.PromptInfo.Builder()
                .setTitle("Cofre dos Mestres da Guilda")
                .setSubtitle("Autentique com sua digital ou reconhecimento facial")
                .setAllowedAuthenticators(
                    BiometricManager.Authenticators.BIOMETRIC_STRONG or
                            BiometricManager.Authenticators.BIOMETRIC_WEAK or
                            BiometricManager.Authenticators.DEVICE_CREDENTIAL
                )
                .build()

            biometricPrompt.authenticate(promptInfo)
        } else {
            Toast.makeText(context, "Sensor biométrico simulado para desenvolvimento!", Toast.LENGTH_SHORT).show()
            onAutenticadoComSucesso()
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("AUTENTICAÇÃO DO COFRE", fontWeight = FontWeight.Bold) },
                navigationIcon = {
                    IconButton(onClick = onVoltarClick) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Voltar", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = Color(0xFF1F1430),
                    titleContentColor = Color.White
                )
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize()
                .padding(32.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Box(
                modifier = Modifier
                    .size(90.dp)
                    .clip(CircleShape)
                    .background(Color(0xFFFFB300).copy(alpha = 0.15f)),
                contentAlignment = Alignment.Center
            ) {
                Text("🔐", fontSize = 42.sp)
            }

            Spacer(modifier = Modifier.height(24.dp))
            Text("ÁREA RESTRITA DA GUILDA", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color.White)
            Text(
                "O Cofre dos Mestres contém armas ancestrais e exige verificação biométrica de identidade do Guardião.",
                textAlign = TextAlign.Center,
                color = Color.Gray,
                fontSize = 13.sp,
                lineHeight = 20.sp,
                modifier = Modifier.padding(horizontal = 12.dp)
            )

            Spacer(modifier = Modifier.height(36.dp))

            Button(
                onClick = { dispararBiometria() },
                modifier = Modifier.fillMaxWidth().height(52.dp),
                shape = RoundedCornerShape(12.dp),
                colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFFB300))
            ) {
                Text("AUTENTICAR COM BIOMETRIA", fontWeight = FontWeight.Black, color = Color.Black)
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaSalaDosTesouros(onVoltarClick: () -> Unit) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("👑 SALA DOS TESOUROS", fontWeight = FontWeight.Black, color = Color(0xFFFFB300)) },
                navigationIcon = {
                    IconButton(onClick = onVoltarClick) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Voltar", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF1F1430))
            )
        },
        containerColor = Color(0xFF120B1E)
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize()
                .padding(20.dp)
        ) {
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(containerColor = Color(0xFF2A1538)),
                shape = RoundedCornerShape(16.dp)
            ) {
                Column(modifier = Modifier.padding(20.dp)) {
                    Text("💎 ARTEFATOS ANCESTRAIS DESBLOQUEADOS", fontWeight = FontWeight.Black, color = Color(0xFFFFB300), fontSize = 14.sp)
                    Spacer(modifier = Modifier.height(12.dp))
                    Text("• 🗡️ Espada da Fênix (Ataque +120)", color = Color.White)
                    Text("• 🛡️ Égide dos Titãs (Defesa +95)", color = Color.White)
                    Text("• 🔮 Orbe dos Elementos Primordiais", color = Color.White)
                }
            }
        }
    }
}

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd proj_aplicacoes_full_stack/projetos/android_p11_herois_biometria

2. Execute a aplicação e os testes

./gradlew build
./gradlew test
# ou abrir no Android Studio e clicar em Run (Shift+F10)

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code ou Android Studio, abra o projeto diretamente pela pasta proj_aplicacoes_full_stack/projetos/android_p11_herois_biometria.