☁️ P07: Master Pokedex (Conectando à Nuvem com Retrofit)
Bem-vindo à segunda missão da Fase 4: Persistência & Dados (Offline First & APIs)! ☁️
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 assentes na 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.
🛠️ Passo 1: Solicitando Acesso à Internet (Manifesto)
Antes de fazer conexões de rede, o Android exige que o app declare que usará a antena de dados/Wi-Fi do aparelho.
- Abra o arquivo
app > src > main > AndroidManifest.xml. - Adicione a linha de permissão logo acima da tag
<application>: ```xml
---
## 🛠️ Passo 2: Adicionando Dependências do Retrofit e Coil (Gradle)
1. Abra `Gradle Scripts > build.gradle (Module :app)`.
2. No bloco `dependencies`, adicione a biblioteca Retrofit, o conversor de JSON GSON e o Coil para imagens:
```gradle
dependencies {
// Retrofit & GSON Converter
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
// Coil para carregamento assíncrono de imagens no Compose
implementation "io.coil-kt:coil-compose:2.5.0"
...
}
Clique em Sync Now.
☁️ Passo 3: Mapeando os Modelos de Dados (PokeApi.kt)
Criaremos a estrutura de rede que representa a resposta do servidor em formato fortemente tipado.
- Crie a pasta de pacotes:
app > java > br.com.curso.masterpokedex > data > network. - Crie o arquivo
PokeApi.kte cole a estrutura completa disponível no Gabarito. Ele mapeia a resposta da lista da API e configura os comandos do garçom (PokeApiService) usando requisições assíncronas do tipoGET.
🧠 Passo 4: Construindo o ViewModel (PokedexViewModel)
O ViewModel centraliza a base URL da PokeAPI (https://pokeapi.co/api/v2/) e inicializa a interface do Retrofit com o tradutor GSON. Ele também monitora as três variáveis chaves de estado de rede: pokemonList (sucesso), isLoading (carregamento) e error (falha).
🎨 Passo 5: Criando a Grade de Cartões e Imagens Asíncronas
Em nossa MainActivity.kt, desenhamos o app usando Scaffold.
Dentro do conteúdo principal, usamos a variável isLoading para exibir um sinalizador de progresso ou a nossa grade LazyVerticalGrid de duas colunas (GridCells.Fixed(2)). Cada cartão renderiza o nome e o número padronizado do Pokémon, enquanto o componente AsyncImage baixa a arte oficial da nuvem de forma invisível.
🛠️ Requisitos Críticos de Configuração e Fechamento
1. URLs HTTPS Obrigatórias
Por padrão, o Android bloqueia conexões inseguras (que usam o protocolo HTTP simples). Certifique-se de que a sua baseUrl em seu ViewModel use https:// obrigatoriamente.
2. Tratamento de Erros
Sempre faça requisições externas envelopadas em blocos try-catch. Caso o celular do usuário fique sem sinal ou em modo avião, a falha de rede gerará uma exceção que fecharia o app (crash) instantaneamente caso não estivesse capturada com segurança.
🏆 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.
- Busca por Nome: Adicione um campo de busca que filtra a lista de Pokémon já carregada pelo nome digitado.
📖 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 kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MasterPokedexTheme {
PokedexApp()
}
}
}
}
@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() {
private val retrofit = Retrofit.Builder()
.baseUrl("https://pokeapi.co/api/v2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService: PokeApiService = retrofit.create(PokeApiService::class.java)
var pokemonList by mutableStateOf<List<PokemonBrief>>(emptyList())
var isLoading by mutableStateOf(false)
var error by mutableStateOf<String?>(null)
fun fetchPokemons() {
isLoading = true
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PokedexApp(viewModel: PokedexViewModel = viewModel()) {
val coroutineScope = rememberCoroutineScope()
// Carregar os dados ao iniciar
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
}
}
val backgroundGradient = Brush.verticalGradient(
colors = listOf(Color(0xFF8E0E00), Color(0xFF1F1C1C)) // Gradiente vermelho escuro/preto
)
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()) {
if (viewModel.isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Color(0xFFFF1744)
)
} else if (viewModel.error != null) {
Text(
text = "Erro: ${viewModel.error}",
color = Color.White,
modifier = Modifier.align(Alignment.Center).padding(16.dp),
textAlign = TextAlign.Center
)
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp)
) {
items(viewModel.pokemonList) { pokemon ->
PokemonCard(pokemon)
}
}
}
}
}
}
}
@Composable
fun PokemonCard(pokemon: PokemonBrief) {
// Extrair ID 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
) {
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
)
}
}
}