☁️ 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:


🎯 Objetivo do Projeto

Criar uma Pokédex virtual completa e dinâmica disposta em duas colunas, contendo:

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


🛠️ Passo 1: Configurando o Projeto no Android Studio

  1. Abra o Android Studio e crie um projeto usando o template Empty Activity (Compose padrão).
  2. Preencha as configurações:
    • Name: Master Pokedex
    • Package Name: br.com.curso.masterpokedex
    • Language: Kotlin.
    • Minimum SDK: API 24 ou superior.
  3. 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á.

  1. No painel Project, navegue até app > src > main e abra o arquivo AndroidManifest.xml.
  2. 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.

  1. No painel Project, vá em Gradle Scripts e abra o arquivo build.gradle (Module :app) (não o do projeto raiz).
  2. 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"
}
  1. 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.Retrofit em um arquivo .kt sem 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:

4.2 Criando o pacote e o arquivo

  1. No painel Project, clique com o botão direito em app > java > br.com.curso.masterpokedex.
  2. Selecione New > Package e digite data.network. Pressione Enter.
  3. Clique com o botão direito na nova pasta network.
  4. 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:


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


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