Índice de contenido
- Incorporando Retrofit y dependencias en nuestro proyecto
- Configuración Inicial
- 1. Data Classes y Modelado
- 2. Sealed Classes: Tipos enumerados con esteroides
- Ventajas de las Sealed Classes
- 3. Retrofit Client y Singleton
- Interfaz de servicio
- Implementación y Pantalla Principal
- Llamada a la API y manejo de estados
- Mostrar resultados y cargar imágenes
- Código Completo
- Conclusión
Retrofit es un cliente REST para desarrollar aplicaciones en Android; es relativamente fácil de usar (lo esperado en entornos con Java), permite agregar convertidores personalizados para mapear los datos obtenidos desde una API REST en formato XML o en él más que famoso JSON en un objeto de una clase personalizada mediante un desearilizador.
Anteriormente vimos cómo crear una notificación en Android Studio
Para poder trabajar con Retrofit en nuestro proyecto en Android Studio necesitamos:
- Una REST API creada en otra plataforma que podamos obtener HTTP; ya hemos visto anteriormente cómo crear una REST API con CodeIgniter.
- Una clase modelo que permita mapear/castear el JSON/XML devuelto por la REST API en nuestro objeto que es lo que se conoce como desearilizador.
- Una clase adaptadora que permite indicar qué objetos van a ser deserializados
- Una interfaz para definir nuestras URLs, anotaciones (GET, POST...) que permiten tener centralizadas las peticiones realizadas a nuestra REST API.
- Realidad la conexión con la API REST.
Teniendo estos elementos señalados anteriormente podemos trabajar perfectamente con Retrofit en nuestro proyecto Android.
Incorporando Retrofit y dependencias en nuestro proyecto
Para poder consumir datos desde una API, como JSONs, y convertirlos en objetos dentro de nuestra app Android, necesitamos añadir algunas dependencias a nuestro proyecto. Esto se hace en el archivo build.gradle (a nivel de módulo).
Hoy en día, la gestión de dependencias en Android se realiza usando implementation en lugar del antiguo método compile (considerado legacy). A continuación, se muestran las dependencias actualizadas para Retrofit y el conversor Gson:
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.google.code.gson:gson:2.13.2'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'Puedes consultar la última versión y la documentación oficial desde el sitio oficial de Retrofit.
Otras librerías para procesar JSONs
Retrofit es flexible y soporta varias librerías para procesar JSON. Dependiendo de tus necesidades, puedes usar alguna de las siguientes:
- Gson:
implementation 'com.squareup.retrofit2:converter-gson:3.0.0' - Moshi:
implementation 'com.squareup.retrofit2:converter-moshi:3.0.0' - Jackson:
implementation 'com.squareup.retrofit2:converter-jackson:3.0.0'
En este experimento emplearemos Gson por su popularidad y simplicidad, pero eres libre de elegir la que mejor se adapte a tu proyecto.
Configuración Inicial
Lo primero es instalar las dependencias necesarias en nuestro archivo build.gradle:
- Retrofit: La librería principal para las peticiones.
- Gson Converter: Para convertir automáticamente el formato JSON a objetos de Kotlin.
- Coil: Para cargar imágenes desde una URL de internet (como los sprites de los Pokémon).
No olvides habilitar el permiso de internet en tu archivo AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />Backend y APIs
En una aplicación real para un cliente, usualmente tendrás que implementar o consumir una capa de Backend. Ya sea usando tecnologías como Laravel (PHP) o Python (Django, FastAPI, Flask), el objetivo es crear una API REST que tu aplicación Android pueda consultar.Para este ejemplo, utilizaremos una API pública y gratuita muy conocida: la PokeAPI. Al enviarle el nombre de un Pokémon, nos devuelve un formato JSON con sus características, el cual debemos traducir a modelos de datos en nuestro código.
1. Data Classes y Modelado
Nuestro objetivo será crear un modelo que represente esa respuesta y mostrar algunos datos en pantalla. Este será un ejemplo mínimo, ya que en una aplicación real hay muchas más capas y archivos involucrados para que todo funcione de forma adecuada.
Lo primero que tenemos es un data class. Aquí definimos un identificador, un nombre y el sprite (la imagen del Pokémon). Recuerda que el propósito de los data class es modelar estructuras de datos, especialmente cuando trabajamos con respuestas de Internet.
En este caso, el Pokémon es un objeto del mundo real que tiene un ID, un nombre y una imagen. Como no necesitamos herencia, polimorfismo ni lógica compleja, los data class son perfectos para este escenario.
El sprite puede ser opcional, por eso utilizamos el signo de interrogación (?). Además, definimos otra clase para el sprite, donde nos interesa únicamente la imagen frontal para mantener el ejemplo sencillo.
data class PokemonResponse(
val id: Int,
val name: String,
val sprites: PokemonSprites?
)
data class PokemonSprites(
val front_default: String?
)2. Sealed Classes: Tipos enumerados con esteroides
Para manejar la interfaz de usuario, empleamos una Sealed Class (PokemonUiState). A diferencia de los ENUM tradicionales, estas nos permiten tener una estructura cerrada pero flexible para manejar los estados de la conexión:
- Idle: Estado inicial.
- Loading: Cuando la petición está en curso.
- Success: Cuando la respuesta es exitosa.
- Error: Cuando falla la conexión o no se encuentra el recurso.
Las sealed classes son similares a los enums, pero más potentes. Son clases cerradas, lo que nos da más control y seguridad en el código. A diferencia de comparar strings o usar enums tradicionales, aquí evitamos errores como valores mal escritos o inconsistencias.
Este tipo de clases se suele usar junto con un when, y más adelante veremos cómo Kotlin nos obliga a manejar todos los casos posibles, lo cual es una gran ventaja.
Ventajas de las Sealed Classes
Un detalle importante es que las sealed classes están muy bien integradas con Kotlin. Por ejemplo, si usamos un when y no cubrimos todos los estados definidos, el compilador nos marcará un error. Esto nos protege de errores en tiempo de ejecución y nos obliga a manejar todos los casos posibles.
/ --- ESTADOS DE LA UI (SEALED CLASS) ---
sealed class PokemonUiState {
object Idle : PokemonUiState()
object Loading : PokemonUiState()
data class Success(val pokemon: PokemonResponse) : PokemonUiState()
data class Error(val message: String) : PokemonUiState()
}3. Retrofit Client y Singleton
A continuación tenemos el cliente de Retrofit, que está implementado como un Singleton. Solo necesitamos una instancia de conexión a Internet, así que no tiene sentido crear varias.
Aquí configuramos el baseUrl, que es la URL raíz de la API de Pokémon. También declaramos la instancia del servicio, que contiene los endpoints disponibles. En este caso solo tenemos uno, para obtener el detalle de un Pokémon, pero en un CRUD real tendrías muchos más.
Retrofit concatena automáticamente el baseUrl con la ruta definida en el servicio. Además, configuramos el convertidor de JSON, que se encarga de transformar las respuestas JSON en objetos Kotlin. Por eso instalamos anteriormente el paquete de Gson.
object RetrofitClient {
private const val BASE_URL = "https://pokeapi.co/api/v2/"
val instance: PokeApiService by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(PokeApiService::class.java)
}
}Interfaz de servicio
La interfaz del servicio no tiene implementación directa. Simplemente define las rutas y las peticiones disponibles. Retrofit se encarga de generar la implementación internamente.
interface PokeApiService {
@GET("pokemon/{name}")
suspend fun getPokemonInfo(@Path("name") name: String): PokemonResponse
}Implementación y Pantalla Principal
Llegamos al MainActivity, donde creamos el componente PokemonScreen. Aquí utilizamos conceptos avanzados de Jetpack Compose:
- Corrutinas: Las conexiones a internet son asíncronas por naturaleza. Usamos rememberCoroutineScope para lanzar la búsqueda sin bloquear la interfaz.
- Estado Reactivo: Usamos mutableStateOf para que la pantalla se recargue automáticamente cuando recibimos la información del Pokémon o si ocurre un error.
Llamada a la API y manejo de estados
Cuando el usuario presiona el botón, llamamos al servicio de Retrofit y actualizamos el estado:
- Pasamos a Loading antes de hacer la llamada.
- Si la respuesta es exitosa, pasamos a Success.
- Si ocurre un error, pasamos a Error.
Estos estados se reflejan directamente en la interfaz gráfica, mostrando un indicador de carga, un mensaje de error o la información del Pokémon.
@Composable
fun PokemonScreen() {
val scope = rememberCoroutineScope()
var pokemonName by remember { mutableStateOf("") }
var uiState by remember { mutableStateOf<PokemonUiState>(PokemonUiState.Idle) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = pokemonName,
onValueChange = { pokemonName = it },
label = { Text("Nombre del Pokémon") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
uiState = PokemonUiState.Loading
scope.launch {
try {
val response = RetrofitClient.instance.getPokemonInfo(pokemonName.lowercase().trim())
uiState = PokemonUiState.Success(response)
} catch (e: Exception) {
Log.e("POKEMON_APP", e.toString())
uiState = PokemonUiState.Error("No se encontró el Pokémon")
}
}
},
modifier = Modifier.padding(top = 8.dp)
) {
Text("Buscar")
}
Spacer(modifier = Modifier.height(20.dp))
when (val state = uiState) {
is PokemonUiState.Loading -> CircularProgressIndicator()
is PokemonUiState.Success -> PokemonDetailCard(state.pokemon)
is PokemonUiState.Error -> Text(state.message, color = Color.Red)
is PokemonUiState.Idle -> Text("Escribe un nombre para empezar")
}
}
}Finalmente, usamos una estructura when para evaluar el estado de nuestra clase sellada. Si el estado es exitoso, llamamos al componente PokemonDetailCard, que utiliza Coil (AsyncImage) para pintar la imagen del Pokémon directamente desde la URL.
when (val state = uiState) {
is PokemonUiState.Loading -> CircularProgressIndicator()
is PokemonUiState.Success -> PokemonDetailCard(state.pokemon)
is PokemonUiState.Error -> Text(state.message, color = Color.Red)
is PokemonUiState.Idle -> Text("Escribe un nombre para empezar")
}Mostrar resultados y cargar imágenes
Cuando el estado es exitoso, mostramos un composable personalizado con la información del Pokémon. Utilizamos una Card, una Column, textos informativos y una imagen cargada desde Internet.
Para cargar imágenes usamos otra librería adicional que ya habíamos instalado. Esto nos permite mostrar la imagen directamente desde la URL del sprite.
@Composable
fun PokemonDetailCard(pokemon: PokemonResponse) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = pokemon.sprites?.front_default,
contentDescription = "Imagen de ${pokemon.name}",
modifier = Modifier.size(150.dp)
)
Text(text = "ID: #${pokemon.id}", style = MaterialTheme.typography.labelMedium)
Text(text = pokemon.name.uppercase(), style = MaterialTheme.typography.headlineMedium)
}
}
}
Código Completo
package com.example.myproyectandroid
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.example.myproyectandroid.ui.theme.MyProyectAndroidTheme
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
// --- MODELOS DE DATOS ---
data class PokemonResponse(
val id: Int,
val name: String,
val sprites: PokemonSprites?
)
data class PokemonSprites(
val front_default: String?
)
// --- ESTADOS DE LA UI (SEALED CLASS) ---
sealed class PokemonUiState {
object Idle : PokemonUiState()
object Loading : PokemonUiState()
data class Success(val pokemon: PokemonResponse) : PokemonUiState()
data class Error(val message: String) : PokemonUiState()
}
// --- CLIENTE RETROFIT (SINGLETON) ---
object RetrofitClient {
private const val BASE_URL = "https://pokeapi.co/api/v2/"
val instance: PokeApiService by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(PokeApiService::class.java)
}
}
interface PokeApiService {
@GET("pokemon/{name}")
suspend fun getPokemonInfo(@Path("name") name: String): PokemonResponse
}
// --- ACTIVIDAD PRINCIPAL ---
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyProyectAndroidTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
PokemonScreen()
}
}
}
}
}
}
@Composable
fun PokemonScreen() {
val scope = rememberCoroutineScope()
var pokemonName by remember { mutableStateOf("") }
var uiState by remember { mutableStateOf<PokemonUiState>(PokemonUiState.Idle) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = pokemonName,
onValueChange = { pokemonName = it },
label = { Text("Nombre del Pokémon") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
uiState = PokemonUiState.Loading
scope.launch {
try {
val response = RetrofitClient.instance.getPokemonInfo(pokemonName.lowercase().trim())
uiState = PokemonUiState.Success(response)
} catch (e: Exception) {
Log.e("POKEMON_APP", e.toString())
uiState = PokemonUiState.Error("No se encontró el Pokémon")
}
}
},
modifier = Modifier.padding(top = 8.dp)
) {
Text("Buscar")
}
Spacer(modifier = Modifier.height(20.dp))
when (val state = uiState) {
is PokemonUiState.Loading -> CircularProgressIndicator()
is PokemonUiState.Success -> PokemonDetailCard(state.pokemon)
is PokemonUiState.Error -> Text(state.message, color = Color.Red)
is PokemonUiState.Idle -> Text("Escribe un nombre para empezar")
}
}
}
@Composable
fun PokemonDetailCard(pokemon: PokemonResponse) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = pokemon.sprites?.front_default,
contentDescription = "Imagen de ${pokemon.name}",
modifier = Modifier.size(150.dp)
)
Text(text = "ID: #${pokemon.id}", style = MaterialTheme.typography.labelMedium)
Text(text = pokemon.name.uppercase(), style = MaterialTheme.typography.headlineMedium)
}
}
}Puedes consultar la documentación oficial y más ejemplos en el siguiente enlace: Retrofit.
Conclusión
De esta forma tan sencilla aprendiste a realizar tu primera conexión a Internet en Android usando Retrofit, además de aplicar conceptos importantes como data class, sealed classes, corrutinas y estado reactivo con Jetpack Compose. Todo esto forma una base sólida para crear aplicaciones modernas y escalables.
El siguiente paso, consisten en aprender a emplear el API de Canvas en Android Studio.