☁️ P07: Master Pokedex (Conectando à Nuvem com Retrofit)
Bem-vindo à segunda missão da Fase 4: Recursos Avançados! ☁️
Este é o projeto que separa oficialmente os desenvolvedores iniciantes dos profissionais de mercado! Você aprenderá a conectar seu aplicativo de forma dinâmica à internet para consumir serviços web (APIs REST). Mapearemos dados estruturados da famosa PokeAPI usando a biblioteca Retrofit combinada com a renderização de imagens externas obtidas diretamente do servidor através do carregador assíncrono inteligente Coil.
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 📘 Cap 13: Conectando ao Mundo (PokeAPI)
- 📘 Cap 14: Dados Pokemon: O Mundo Real
- 🏗️ Projeto anterior: P06: Meus Jogos Favoritos
🎯 Objetivo do Projeto
Criar uma Pokédex virtual completa e dinâmica disposta em duas colunas, contendo:
- Consumo Dinâmico: Fazer requisições assíncronas à internet para buscar a lista dos primeiros 20 Pokémons.
- Gerenciamento de Estados do Servidor: Exibir telas dinâmicas de carregamento (
CircularProgressIndicatorgirando) ou mensagens de erro robustas caso a conexão do celular falhe. - Grid Rígido Moderno: Renderizar as informações em células de grade otimizadas (
LazyVerticalGrid). - Carregamento Inteligente de Imagens: Extrair o número identificador de cada criatura no link da API e carregar as artes oficiais em alta resolução armazenadas na nuvem usando o componente
AsyncImage.
graph TD
A[Início / Inicializar PokedexApp] --> B[LaunchedEffect dispara requisição em background]
B --> C[Exibir CircularProgressIndicator]
C --> D[Retrofit GET pokemon?limit=20]
D --> E{Requisição com Sucesso?}
E -- Não --> F[Salvar erro no estado & Exibir Mensagem de Erro]
E -- Sim --> G[Mapear lista de Pokemons para pokemonList]
G --> H[Ocultar loading & Renderizar LazyVerticalGrid]
H --> I[Para cada item: PokemonCard]
I --> J[Extrair ID da URL & Calcular URL da Imagem no GitHub]
J --> K[Carregar Imagem via Coil AsyncImage & Exibir Nome]
📖 Dicionário do Projeto
- API (Application Programming Interface): Um conjunto de endereços e regras na internet que servem como porta de comunicação para que aplicativos solicitem ou enviem dados de servidores corporativos.
- Retrofit: A biblioteca de rede HTTP oficial e mais famosa para Android criada pela Square. Traduz interfaces puras do Kotlin em requisições de servidores automatizadas.
- GSON / Converter Factory: Uma biblioteca desenvolvida pelo Google que traduz automaticamente textos no formato JSON enviados pelo servidor em classes Kotlin repletas de atributos.
- PokeAPI: Uma das APIs abertas e públicas mais populares do mundo dos games que armazena dados estruturados sobre Pokémons, golpes, habilidades e tipos.
- Coil (AsyncImage): O carregador de imagens nativo mais moderno e performático do ecossistema Compose. Gerencia requisições de fotos web, cria cache automático em disco e carrega dados em Threads paralelas para evitar engasgos visuais.
- LazyVerticalGrid: Componente Compose especialista em renderizar dados estruturados em formato de colunas (grid). Excelente para catálogos e galerias de fotos.
- data class: Classe Kotlin especial criada para armazenar dados. O compilador gera automaticamente os métodos
equals(),hashCode()etoString(), poupando código repetitivo. - suspend fun: Palavra-chave Kotlin que marca uma função como assíncrona — ela pode pausar sua execução enquanto aguarda uma resposta da rede, sem bloquear a Thread principal e congelar a tela.
🛠️ Passo 1: Configurando o Projeto no Android Studio
- Abra o Android Studio e crie um projeto usando o template Empty Activity (Compose padrão).
- Preencha as configurações:
- Name:
Master Pokedex - Package Name:
br.com.curso.masterpokedex - Language: Kotlin.
- Minimum SDK: API 24 ou superior.
- Name:
- Aguarde o Gradle sincronizar o projeto (barra de progresso no rodapé do Android Studio).
🛠️ Passo 2: Solicitando Acesso à Internet (Manifesto)
Antes de fazer conexões de rede, o Android exige que o app declare explicitamente que usará a antena de dados/Wi-Fi do aparelho. Sem essa declaração, qualquer chamada de rede lançará uma SecurityException e o app fechará.
- No painel Project, navegue até
app > src > maine abra o arquivoAndroidManifest.xml. - Adicione a linha de permissão logo acima da tag
<application>, deixando o arquivo assim:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="Master Pokedex"
android:theme="@style/Theme.MasterPokedex">
...
</application>
</manifest>
Por que acima de
<application>? A tag<uses-permission>deve ficar no nível do<manifest>, não dentro da<application>. Colocá-la no lugar errado gera um warning de build e pode não funcionar em versões antigas do Android.
🛠️ Passo 3: Adicionando Dependências do Retrofit e Coil (Gradle)
As bibliotecas Retrofit e Coil não vêm com o Android Studio — precisamos baixá-las pelo sistema de build Gradle.
- No painel Project, vá em
Gradle Scriptse abra o arquivobuild.gradle (Module :app)(não o do projeto raiz). - Localize o bloco
dependencies { ... }e adicione as três novas linhas abaixo das dependências já existentes:
dependencies {
// ... dependências que já existem (compose-bom, material3, activity-compose, etc.) ...
// Retrofit: cliente HTTP para fazer chamadas à API
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Converter GSON: traduz o JSON da resposta em classes Kotlin
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
// Coil: carregador assíncrono de imagens para Jetpack Compose
implementation "io.coil-kt:coil-compose:2.6.0"
}
- Clique no botão Sync Now que aparece na barra amarela no topo do editor para baixar as bibliotecas. Aguarde a conclusão (pode levar alguns segundos).
Como verificar se o sync funcionou? Após o sync, você deve conseguir importar
import retrofit2.Retrofitem um arquivo.ktsem erro de símbolo não encontrado.
☁️ Passo 4: Mapeando os Modelos de Dados (PokeApi.kt)
Antes de fazer chamadas de rede, precisamos criar um mapa do tesouro — estruturas de dados que descrevem exatamente o formato JSON que a PokeAPI devolve. O Retrofit usa esse mapa para traduzir automaticamente o JSON em objetos Kotlin.
4.1 Entendendo a resposta da API
Abra seu navegador e acesse: https://pokeapi.co/api/v2/pokemon?limit=20
Você verá um JSON parecido com este:
{
"count": 1302,
"results": [
{ "name": "bulbasaur", "url": "https://pokeapi.co/api/v2/pokemon/1/" },
{ "name": "ivysaur", "url": "https://pokeapi.co/api/v2/pokemon/2/" },
...
]
}
Precisamos criar data classes Kotlin que espelhem exatamente esse formato:
- A resposta raiz tem um campo
resultsque é uma lista. - Cada item da lista tem um
namee umaurl.
4.2 Criando o pacote e o arquivo
- No painel Project, clique com o botão direito em
app > java > br.com.curso.masterpokedex. - Selecione New > Package e digite
data.network. Pressione Enter. - Clique com o botão direito na nova pasta
network. - Selecione New > Kotlin Class/File, escolha File e nomeie
PokeApi. Pressione Enter.
4.3 Definindo os modelos de dados
Cole o seguinte código no arquivo PokeApi.kt recém-criado:
package br.com.curso.masterpokedex.data.network
import com.google.gson.annotations.SerializedName
import retrofit2.http.GET
import retrofit2.http.Path
// ─── PARTE 1: MODELOS DE DADOS (o "mapa" do JSON) ───────────────────────────
// Representa o objeto raiz retornado pela API na listagem
data class PokemonListResponse(
val results: List<PokemonBrief> // campo "results" do JSON
)
// Representa cada item da lista (nome + URL com o ID embutido)
data class PokemonBrief(
val name: String, // ex: "bulbasaur"
val url: String // ex: "https://pokeapi.co/api/v2/pokemon/1/"
)
// Modelos usados no Desafio: buscar detalhes de um Pokémon específico
data class PokemonDetailResponse(
val id: Int,
val name: String,
@SerializedName("sprites") val sprites: SpriteResponse // "sprites" no JSON → sprites em Kotlin
)
data class SpriteResponse(
@SerializedName("front_default") val frontDefault: String // "front_default" no JSON → frontDefault
)
// ─── PARTE 2: O GARÇOM (interface de requisições) ────────────────────────────
// O Retrofit usa essa interface para gerar automaticamente o código de rede
interface PokeApiService {
// GET /pokemon?limit=20 → retorna a lista dos 20 primeiros
@GET("pokemon?limit=20")
suspend fun getList(): PokemonListResponse
// GET /pokemon/{name} → retorna detalhes de um Pokémon pelo nome
@GET("pokemon/{name}")
suspend fun getDetail(@Path("name") name: String): PokemonDetailResponse
}
Explicação das anotações:
@GET("pokemon?limit=20")— instrui o Retrofit a fazer uma requisição HTTP GET para o endpointpokemon?limit=20(relativo àbaseUrlque configuraremos no ViewModel).suspend fun— garante que a chamada de rede rode em uma coroutine, sem bloquear a Thread da UI.@SerializedName("front_default")— necessário quando o nome do campo no JSON usasnake_case, diferente do padrãocamelCasedo Kotlin. O GSON usa essa anotação para fazer o mapeamento correto.@Path("name")— substitui o trecho{name}na URL pelo valor passado no parâmetro da função.
🧠 Passo 5: Construindo o ViewModel (PokedexViewModel)
O ViewModel é o gerente de dados da tela. Ele inicializa o cliente Retrofit, expõe o serviço de API e mantém três variáveis de estado que a interface observa em tempo real.
Abra o arquivo app > java > br.com.curso.masterpokedex > MainActivity.kt e substitua todo o conteúdo pelos blocos abaixo (começando pelo ViewModel, antes da classe MainActivity).
5.1 Importações necessárias
No topo do MainActivity.kt, certifique-se de ter todos estes imports:
package br.com.curso.masterpokedex
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import br.com.curso.masterpokedex.data.network.PokeApiService
import br.com.curso.masterpokedex.data.network.PokemonBrief
import coil.compose.AsyncImage
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
5.2 O ViewModel
class PokedexViewModel : ViewModel() {
// Instância do Retrofit com baseUrl e conversor GSON
private val retrofit = Retrofit.Builder()
.baseUrl("https://pokeapi.co/api/v2/") // endereço base da PokeAPI
.addConverterFactory(GsonConverterFactory.create()) // plugin de tradução JSON→Kotlin
.build()
// Cria o "garçom" a partir da interface que definimos em PokeApi.kt
val apiService: PokeApiService = retrofit.create(PokeApiService::class.java)
// ── Os três estados reativos que a UI observa ──────────────────────────
var pokemonList by mutableStateOf<List<PokemonBrief>>(emptyList())
// ↑ lista preenchida após resposta bem-sucedida
var isLoading by mutableStateOf(false)
// ↑ true enquanto aguarda resposta da rede
var error by mutableStateOf<String?>(null)
// ↑ mensagem de erro caso a chamada falhe; null quando tudo está ok
}
Por que mutableStateOf e não LiveData? No Jetpack Compose, mutableStateOf é a forma idiomática de criar estados que acionam recomposição automática da UI quando seus valores mudam — exatamente como um placar de jogo que atualiza o número na tela ao marcar um ponto.
🎨 Passo 6: Construindo a Interface da Pokédex (MainActivity.kt)
Com o ViewModel pronto, construímos a interface visual em três partes: o Tema, a Tela Principal (PokedexApp) e os Cartões de Pokémon (PokemonCard).
6.1 A Activity e o Tema Personalizado
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MasterPokedexTheme { // aplica as cores da Pokédex em todo o app
PokedexApp()
}
}
}
}
/**
* Tema customizado com paleta Pokédex (vermelho + verde neon)
*/
@Composable
fun MasterPokedexTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFFF1744), // vermelho Pokédex
secondary = Color(0xFF00E676), // verde neon
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E)
),
content = content
)
}
6.2 A Tela Principal com Gerenciamento de Estados
A tela faz a requisição na primeira vez que é exibida (via LaunchedEffect) e usa um bloco when para decidir o que mostrar dependendo do estado atual.
/**
* Tela principal da Pokédex com gerenciamento de estados de rede e grade de cartões.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PokedexApp(viewModel: PokedexViewModel = viewModel()) {
// LaunchedEffect(Unit): executa o bloco UMA ÚNICA VEZ ao entrar na tela
LaunchedEffect(Unit) {
viewModel.isLoading = true // 1. ativa o indicador de carregamento
try {
val response = viewModel.apiService.getList() // 2. faz a chamada de rede
viewModel.pokemonList = response.results // 3. salva a lista no estado
} catch (e: Exception) {
viewModel.error = e.message // 4. em caso de falha, salva a mensagem de erro
} finally {
viewModel.isLoading = false // 5. desativa o indicador (sempre, com ou sem erro)
}
}
// Gradiente vermelho escuro → preto como fundo da tela
val backgroundGradient = Brush.verticalGradient(
colors = listOf(Color(0xFF8E0E00), Color(0xFF1F1C1C))
)
Box(modifier = Modifier.fillMaxSize().background(backgroundGradient)) {
Scaffold(
containerColor = Color.Transparent, // deixa o gradiente aparecer atrás
topBar = {
TopAppBar(
title = {
Text(
"MASTER POKÉDEX",
fontWeight = FontWeight.Black,
letterSpacing = 2.sp,
color = Color.White
)
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
)
}
) { padding ->
Box(modifier = Modifier.padding(padding).fillMaxSize()) {
when {
// Estado 1 — Carregando: exibe o anel giratório centralizado
viewModel.isLoading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Color(0xFFFF1744)
)
// Estado 2 — Erro de rede: exibe a mensagem para o usuário
viewModel.error != null -> Text(
text = "Erro: ${viewModel.error}",
color = Color.White,
modifier = Modifier.align(Alignment.Center).padding(16.dp),
textAlign = TextAlign.Center
)
// Estado 3 — Sucesso: exibe a grade de cartões
else -> LazyVerticalGrid(
columns = GridCells.Fixed(2), // sempre 2 colunas
contentPadding = PaddingValues(8.dp) // espaço nas bordas da grade
) {
items(viewModel.pokemonList) { pokemon ->
PokemonCard(pokemon)
}
}
}
}
}
}
}
Por que when em vez de if/else? O when em Kotlin é mais legível para múltiplas condições e deixa claro que apenas um dos três estados pode estar ativo por vez — uma analogia com os estados de uma batalha Pokémon: ou está carregando, ou houve erro, ou os dados estão disponíveis.
6.3 O Cartão de Pokémon com Imagem da Nuvem
O PokemonCard recebe um PokemonBrief (nome + URL), extrai o ID numérico da URL e monta o endereço da arte oficial hospedada no repositório do PokeAPI no GitHub.
/**
* Cartão individual de Pokémon com imagem oficial e número formatado
*/
@Composable
fun PokemonCard(pokemon: PokemonBrief) {
// A URL tem o formato: "https://pokeapi.co/api/v2/pokemon/25/"
// split("/") → ["https:", "", "pokeapi.co", "api", "v2", "pokemon", "25", ""]
// dropLast(1) remove o "" do final → [..., "25"]
// lastOrNull() pega o último elemento → "25"
val id = pokemon.url.split("/").dropLast(1).lastOrNull() ?: "1"
// Arte oficial em PNG hospedada no GitHub do projeto PokeAPI
val imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png"
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.05f)),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// AsyncImage (Coil): baixa e exibe a imagem da nuvem de forma assíncrona
// Enquanto carrega, a área fica em branco; quando pronta, aparece suavemente
AsyncImage(
model = imageUrl,
contentDescription = pokemon.name,
modifier = Modifier.size(100.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = pokemon.name.uppercase(), // "bulbasaur" → "BULBASAUR"
fontWeight = FontWeight.Bold,
color = Color.White,
fontSize = 14.sp,
textAlign = TextAlign.Center
)
Text(
text = "#${id.padStart(3, '0')}", // "1" → "#001", "25" → "#025"
color = Color.Gray,
fontSize = 12.sp
)
}
}
}
Como padStart(3, '0') funciona? Garante que o número sempre ocupe 3 dígitos, preenchendo com zeros à esquerda se necessário. Pikachu é o #025, não o #25 — esse é o padrão oficial da Pokédex Nacional.
🛠️ Requisitos Críticos de Configuração e Fechamento
1. URLs HTTPS Obrigatórias
Por padrão, o Android bloqueia conexões inseguras (protocolo HTTP simples). Certifique-se de que a baseUrl no ViewModel use https:// obrigatoriamente:
.baseUrl("https://pokeapi.co/api/v2/") // ✅ correto
.baseUrl("http://pokeapi.co/api/v2/") // ❌ bloqueado pelo Android
2. Tratamento de Erros é Obrigatório
Sempre envolva chamadas de rede em blocos try-catch. Sem essa proteção, qualquer falha de rede (modo avião, sem Wi-Fi, servidor indisponível) causaria um crash imediato do app:
try {
val response = viewModel.apiService.getList()
viewModel.pokemonList = response.results
} catch (e: Exception) {
viewModel.error = e.message // exibe a mensagem ao usuário, sem crash
} finally {
viewModel.isLoading = false // garante que o loading sempre pare
}
3. A baseUrl deve terminar com /
O Retrofit exige que a URL base sempre termine com barra. Caso contrário, ele lançará uma IllegalArgumentException na inicialização do app.
🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
- Tela de Detalhes: Ao clicar em um card, navegue para uma nova tela mostrando altura, peso e tipo do Pokémon. Dica: use
getDetail(), que já está definido emPokeApiService, e adicioneNavigation Composeao projeto. - Busca por Nome: Adicione um
OutlinedTextFieldno topo da tela e filtreviewModel.pokemonListpelo nome digitado usando.filter { it.name.contains(query, ignoreCase = true) }.
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos Kotlin completos abaixo para validar sua implementação.
📄 Código de Serviços Web Completo (PokeApi.kt)
Disponível em: app/src/main/java/br/com/curso/masterpokedex/data/network/PokeApi.kt
package br.com.curso.masterpokedex.data.network
import com.google.gson.annotations.SerializedName
import retrofit2.http.GET
import retrofit2.http.Path
// 1. MODELOS DE DADOS (Dicionário da API)
data class PokemonListResponse(
val results: List<PokemonBrief>
)
data class PokemonBrief(
val name: String,
val url: String
)
data class PokemonDetailResponse(
val id: Int,
val name: String,
@SerializedName("sprites") val sprites: SpriteResponse
)
data class SpriteResponse(
@SerializedName("front_default") val frontDefault: String
)
// 2. O GARÇOM (Retrofit Interface)
interface PokeApiService {
@GET("pokemon?limit=20")
suspend fun getList(): PokemonListResponse
@GET("pokemon/{name}")
suspend fun getDetail(@Path("name") name: String): PokemonDetailResponse
}
📄 Código de Lógica e Tela Completa (MainActivity.kt)
Disponível em: app/src/main/java/br/com/curso/masterpokedex/MainActivity.kt
package br.com.curso.masterpokedex
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import br.com.curso.masterpokedex.data.network.PokeApiService
import br.com.curso.masterpokedex.data.network.PokemonBrief
import coil.compose.AsyncImage
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MasterPokedexTheme {
PokedexApp()
}
}
}
}
/**
* Tema customizado com paleta Pokédex (vermelho + verde neon)
*/
@Composable
fun MasterPokedexTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFFF1744), // Vermelho Pokedex
secondary = Color(0xFF00E676),
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E)
),
content = content
)
}
class PokedexViewModel : ViewModel() {
// Instância do Retrofit com baseUrl e conversor GSON
private val retrofit = Retrofit.Builder()
.baseUrl("https://pokeapi.co/api/v2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
// Expõe o service para ser chamado pela UI
val apiService: PokeApiService = retrofit.create(PokeApiService::class.java)
// Estados reativos que a UI observa
var pokemonList by mutableStateOf<List<PokemonBrief>>(emptyList())
var isLoading by mutableStateOf(false)
var error by mutableStateOf<String?>(null)
}
/**
* Tela principal da Pokédex com gerenciamento de estados de rede e grade de cartões.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PokedexApp(viewModel: PokedexViewModel = viewModel()) {
// Executa a requisição uma única vez ao entrar na tela
LaunchedEffect(Unit) {
viewModel.isLoading = true
try {
val response = viewModel.apiService.getList()
viewModel.pokemonList = response.results
} catch (e: Exception) {
viewModel.error = e.message
} finally {
viewModel.isLoading = false
}
}
// Gradiente vermelho escuro → preto
val backgroundGradient = Brush.verticalGradient(
colors = listOf(Color(0xFF8E0E00), Color(0xFF1F1C1C))
)
Box(modifier = Modifier.fillMaxSize().background(backgroundGradient)) {
Scaffold(
containerColor = Color.Transparent,
topBar = {
TopAppBar(
title = {
Text(
"MASTER POKÉDEX",
fontWeight = FontWeight.Black,
letterSpacing = 2.sp,
color = Color.White
)
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
)
}
) { padding ->
Box(modifier = Modifier.padding(padding).fillMaxSize()) {
when {
// Estado: carregando
viewModel.isLoading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Color(0xFFFF1744)
)
// Estado: erro de rede
viewModel.error != null -> Text(
text = "Erro: ${viewModel.error}",
color = Color.White,
modifier = Modifier.align(Alignment.Center).padding(16.dp),
textAlign = TextAlign.Center
)
// Estado: lista carregada
else -> LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp)
) {
items(viewModel.pokemonList) { pokemon ->
PokemonCard(pokemon)
}
}
}
}
}
}
}
/**
* Cartão individual de Pokémon com imagem oficial e número formatado
*/
@Composable
fun PokemonCard(pokemon: PokemonBrief) {
// Extrai o ID numérico da URL: "https://pokeapi.co/api/v2/pokemon/1/" -> "1"
val id = pokemon.url.split("/").dropLast(1).lastOrNull() ?: "1"
val imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png"
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.05f)),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Carrega a arte oficial diretamente da nuvem (Coil gerencia cache)
AsyncImage(
model = imageUrl,
contentDescription = pokemon.name,
modifier = Modifier.size(100.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = pokemon.name.uppercase(),
fontWeight = FontWeight.Bold,
color = Color.White,
fontSize = 14.sp,
textAlign = TextAlign.Center
)
Text(
text = "#${id.padStart(3, '0')}",
color = Color.Gray,
fontSize = 12.sp
)
}
}
}