Saltar al contenido
K
Jetpack y arquitecturaPaso a paso35 min de lectura · 8 min de máquina Intermedio

Retrofit y la primera llamada

Al terminar: Instalar Retrofit y OkHttp, hacer la primera llamada real y dejar en Marcador la interfaz de la API.

Al terminar esta lección tendrás Retrofit y OkHttp instalados en Marcador, el permiso de internet declarado, habrás visto en un ejercicio tres tareas reales que llegan desde un servidor y dejarás escrita en Marcador la interfaz que habla con la liga (todavía sin pantalla): la lección siguiente la usa.

Importa porque hacer una petición a mano es mucho trabajo (abrir la conexión, escribir el papel, esperar, leer el sobre, convertir el JSON), y estas dos librerías lo reducen a llamar a una función. Temas: Retrofit y OkHttp, el cliente con timeouts, instalar librerías, el permiso INTERNET y la primera llamada real. Lo que es una petición, un código de estado o un DTO se explicó en Red: HTTP y JSON.

Retrofit y OkHttp: la petición como una función

Dos librerías se reparten el trabajo:

  • OkHttp es el motor: abre las conexiones, envía la petición, espera la respuesta y respeta el timeout.
  • Retrofit es la fachada: tú escribes una interfaz de Kotlin con anotaciones, y Retrofit fabrica, mientras la app funciona, un objeto que cumple esa interfaz y que hace la petición cuando llamas a cada función.

En la interfaz, cada función es una petición. La anotación (@GET) dice el método y la ruta; los parámetros de la función se convierten en parámetros de la URL (@Query); y el tipo que devuelve es el del cuerpo ya convertido.

El ejemplo monta, además, el motor con sus timeouts y se lo da a Retrofit. Es solo para leer: necesita las librerías instaladas y un servidor real, que verás en el ejercicio.

ejemplo mínimo: una API de biblioteca con Retrofit y OkHttp (con comentarios)
// Los DTO del cuerpo: @Serializable permite convertir JSON en estas clases
@Serializable data class LibroDto(val id: Int, val title: String, val author: AutorDto)
@Serializable data class AutorDto(val name: String)
 
interface BibliotecaApi {
    // GET https://biblioteca.example/libros?limite=3
    // @GET: el método y la ruta. @Query: el parámetro tras el «?».
    // suspend: la función espera la respuesta sin bloquear la pantalla
    @GET("libros") suspend fun libros(@Query("limite") limite: Int): List<LibroDto>
}
 
// 1. El MOTOR. Se monta «paso a paso»: Builder() empieza, cada llamada con punto
//    ajusta una cosa y build() termina y entrega el cliente ya hecho.
val motor = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS) // máximo para ABRIR la conexión con el servidor
    .readTimeout(10, TimeUnit.SECONDS)    // máximo para esperar los DATOS una vez abierta
    .build()
// (TimeUnit.SECONDS es la unidad: «10, segundos». Sin ella no se sabría si son 10 ms o 10 min.)
 
// 2. La FACHADA, montada igual, paso a paso
val api = Retrofit.Builder()
    .baseUrl("https://biblioteca.example/")        // la parte común de todas las URL, acabada en «/»
    .client(motor)                                 // el motor del paso 1: Retrofit hace las peticiones con él
    .addConverterFactory(                          // el traductor JSON <-> clases
        Json { ignoreUnknownKeys = true }          // ignora los campos del JSON que el DTO no declara
            .asConverterFactory("application/json; charset=UTF-8".toMediaType())
    )
    .build()
    .create(BibliotecaApi::class.java)             // «fabrícame un objeto que cumpla BibliotecaApi»
 
// En un ViewModel: api.libros(3) devuelve la lista ya convertida

Cosas del ejemplo que conviene fijar:

  • X::class.java es el «carnet» de la interfaz: con él Retrofit sabe cuál es la que debe fabricar.
  • "application/json; charset=UTF-8".toMediaType() convierte ese texto en un tipo de contenido y dice «lo que viaja es JSON, escrito en UTF-8» (la forma de escribir tildes y eñes). Su import lleva la palabra Companion (okhttp3.MediaType.Companion.toMediaType) porque la función vive en el companion object de MediaType (un companion object es el bloque de una clase donde viven las funciones que se piden a la clase misma, MediaType.…, y no a un objeto suyo); Android Studio propone el import solo con Alt+Enter (Option+Enter en Mac).
  • suspend y el hilo principal. Con suspend, Retrofit deja que OkHttp espere en sus propios hilos, así que no hace falta withContext(Dispatchers.IO).
  • La URL base acaba en /. Retrofit le pega la ruta de cada función.
  • Los dos timeouts son los de la lección anterior: si el servidor no abre la conexión o no manda los datos a tiempo, la llamada falla con una IOException en vez de esperar sin fin.

Prepara el proyecto · Instala Retrofit y el permiso de internet

Objetivo. Que :app tenga Retrofit, su traductor de JSON y OkHttp, y que el manifiesto declare el permiso de internet. El ejercicio de después ya los usa.

Es la receta de Los ficheros de Gradle de tu proyecto: versión en el catálogo, librería en el catálogo y implementation en el módulo. El plugin de serialización no hace falta: ya está.

app → manifests
AndroidManifest.xmlse modifica: el permiso
Gradle Scripts
libs.versions.toml (Version Catalog "libs")se modifica: [versions] y [libraries]
build.gradle.kts (Module :app)se modifica: dependencies
Los tres ficheros de esta instalación, tal como los ves en la vista Android.

1 · Qué instalas. Tres librerías:

  • retrofit (com.squareup.retrofit2): la interfaz con anotaciones.
  • converter-kotlinx-serialization: el traductor entre JSON y tus clases @Serializable; es el que ofrece asConverterFactory.
  • okhttp (com.squareup.okhttp3): el motor. Retrofit ya trae OkHttp dentro (una librería suele traer otras dentro), pero en una versión más antigua. Declararlo aparte hace que se use la versión de tu catálogo, y la necesitas porque la app construirá el motor ella misma, con sus timeouts.

2 · Catálogo. Abre Gradle Scripts → libs.versions.toml (Version Catalog "libs") y añade las líneas al final de cada sección:

catálogo: líneas nuevas (con comentarios)
[versions]
# Retrofit (la API como una interfaz) sobre OkHttp (el motor HTTP)
retrofit = "3.0.0"
okhttp = "5.3.2"
 
[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
# Comparte versión con retrofit: se publican juntas
retrofit-converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }

3 · Módulo :app. Abre Gradle Scripts → build.gradle.kts (Module :app) y añade tres líneas dentro de dependencies, junto a las demás:

build.gradle.kts (Module :app): líneas nuevas (con comentarios)
dependencies {
    // … tus dependencias, sin tocar
    implementation(libs.retrofit)                                   // la interfaz con anotaciones
    implementation(libs.retrofit.converter.kotlinx.serialization)   // JSON <-> clases (el plugin ya está)
    implementation(libs.okhttp)                                     // el motor; manda la versión del catálogo
}

4 · El permiso. Android no deja que una app use internet sin declararlo. Es un permiso normal: se concede al instalar, sin diálogo (la tabla de Permisos ya lo lista así). Abre app → manifests → AndroidManifest.xml y añade esta línea dentro de manifest, antes de application, junto al permiso de notificaciones:

AndroidManifest.xml: la línea nueva
    <uses-permission android:name="android.permission.INTERNET" />

5 · Sync Now. Pulsa Sync Now. Debe terminar sin rojo; la primera vez descarga las librerías, así que necesita internet.

6 · Comprueba. Abre la Terminal (View → Tool Windows → Terminal) y pregunta a Gradle qué trae el módulo. El símbolo | pasa la salida del primer comando al segundo, y grep -E "retrofit|okhttp" se queda solo con las líneas que contienen una de esas dos palabras:

./gradlew :app:dependencies --configuration gratisDebugRuntimeClasspath | grep -E "retrofit|okhttp"

Gradle dibuja las dependencias como un árbol: +--- y \--- son ramas, y lo que cuelga con más sangría viene dentro de la línea de arriba; (*) quiere decir «esta ya se mostró antes». Entre las líneas que salen deben estar estas (las demás son Retrofit repitiéndose; -> quiere decir «se pidió la 4.12.0 y se usa la 5.3.2»; la última puede empezar por \--- en vez de +--- si es la última dependencia de la lista):

+--- com.squareup.retrofit2:retrofit:3.0.0
|    +--- com.squareup.okhttp3:okhttp:4.12.0 -> 5.3.2
|    |    \--- com.squareup.okhttp3:okhttp-android:5.3.2
+--- com.squareup.retrofit2:converter-kotlinx-serialization:3.0.0
+--- com.squareup.okhttp3:okhttp:5.3.2 (*)

Tu primera petición real: tres tareas

Reto · Ejercicio: tres tareas desde internet

En la carpeta ejercicios de tu proyecto, crea la subcarpeta tareas_en_red: clic derecho sobre ejercicios → New → Package → añade .tareas_en_red al final del nombre que aparece ya escrito (con el punto delante). Dentro, New → Kotlin Class/File → File → TareasEnRed. La primera línea debe ser package com.sebastiannarvaez.marcador.ejercicios.tareas_en_red.

Escribe una pantalla que pida a https://jsonplaceholder.typicode.com/todos?_limit=3 las tres primeras tareas. Cada tarea del JSON tiene un número (id), un texto (title) y si está hecha (completed): necesitas un DTO con esos tres campos y una interfaz de Retrofit cuya función recibe el límite como parámetro de consulta (el parámetro se llama _limit). Usa el ejemplo de la biblioteca como molde para montar Retrofit; aquí puedes saltarte el motor con timeouts, que viene en el paso.

La pantalla muestra «Cargando…» mientras espera y después una línea por tarea con el formato «número. título · hecha» o «número. título · pendiente». Si no hay red, muestra «Sin conexión». Añade su vista previa.

El panel de vista previa no tiene internet: ejecuta la vista previa en el emulador (triángulo verde junto a @Preview → Run, como en Estado en Compose). Debe verse (los títulos están en latín de relleno: son datos de prueba del servidor):

1. delectus aut autem · pendiente
2. quis ut nam facilis et officia qui · pendiente
3. fugiat veniam minus · pendiente

Después activa el modo avión del emulador (arrastra dos veces desde el borde de arriba para abrir los ajustes rápidos y pulsa el icono del avión) y vuelve a ejecutarla: debe decir «Sin conexión». Al terminar, quita el modo avión y elige otra vez app en el selector de arriba. No borres la subcarpeta.

Pistas, solo con lo visto: el estado de la pantalla es un texto con remember { mutableStateOf(…) }; una función suspend se llama dentro de LaunchedEffect(Unit) (Efectos en Compose: LaunchedEffect); try devuelve el valor de su última línea, así que puedes escribir texto = try { … } catch (e: …) { … }; joinToString("\n") une una lista de textos en uno solo, con un salto de línea entre ellos (Guardar y leer JSON con Json); y sin red, Retrofit lanza una IOException, que se atrapa con catch.

Ver solución
app/src/main/java/com/sebastiannarvaez/marcador/ejercicios/tareas_en_red/TareasEnRed.kt
package com.sebastiannarvaez.marcador.ejercicios.tareas_en_red
 
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
 
@Serializable
data class TareaDto(val id: Int, val title: String, val completed: Boolean)
 
interface TareasApi {
    @GET("todos")
    suspend fun tareas(@Query("_limit") limite: Int): List<TareaDto>
}
 
private val api: TareasApi = Retrofit.Builder()
    .baseUrl("https://jsonplaceholder.typicode.com/")
    .addConverterFactory(Json { ignoreUnknownKeys = true }.asConverterFactory("application/json; charset=UTF-8".toMediaType()))
    .build()
    .create(TareasApi::class.java)
 
@Composable
fun TareasEnRed(modifier: Modifier = Modifier) {
    var texto by remember { mutableStateOf("Cargando…") }
    LaunchedEffect(Unit) {
        texto = try {
            api.tareas(3).joinToString("\n") { "${it.id}. ${it.title} · ${if (it.completed) "hecha" else "pendiente"}" }
        } catch (e: java.io.IOException) {
            "Sin conexión"
        }
    }
    Column(modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text(texto) }
}
 
@Preview(showBackground = true)
@Composable
private fun TareasEnRedPreview() {
    MaterialTheme { TareasEnRed() }
}

@Query("_limit") convierte el parámetro de la función en ?_limit=3. ignoreUnknownKeys deja pasar userId, que el DTO no declara. Una IOException es el fallo «no hubo respuesta» (sin red, servidor inexistente o timeout); una respuesta con código de error es otra cosa, y la verás en POST y errores de red.

Paso · La interfaz de la liga en Marcador

Objetivo. Dejar escrita la parte de Marcador que habla con el servidor: los DTO de los árbitros, la interfaz LigaApi con su única petición, el motor OkHttp con timeout y la función que arma Retrofit. Todavía no hay pantalla: la app se ve igual que antes. La lección siguiente la usa.

app → kotlin+java
com.sebastiannarvaez.marcador
core
networkcarpeta nueva
LigaApi.ktse crea
Un fichero nuevo, en una carpeta nueva, en la vista Android.

Crea el fichero. Con el selector en Android, despliega app → kotlin+java → com.sebastiannarvaez.marcador. Clic derecho sobre com.sebastiannarvaez.marcador → New → Package; el campo ya trae com.sebastiannarvaez.marcador: escribe .core.network al final (con el punto delante). core ya existe (la base de datos vive en core.database), así que network aparece como una carpeta más dentro de core. Clic derecho sobre network → New → Kotlin Class/File → File → LigaApi. Se abre con su línea package.

LigaApi va en core/network porque no es de ninguna feature: es la puerta de la app hacia el servidor, y la usarán las dos features que hablan con él (árbitros y crónica). Lo que no es de ninguna feature (la red, la base de datos, los errores) va en core, y network es la carpeta de lo que habla con el servidor.

Piezas que ya conoces. @Serializable y Json (JSON e instalar kotlinx.serialization), los valores por defecto de los parámetros (Funciones y parámetros), las interfaces (Herencia, interfaces y composición) y el montaje con Builder y .client(…) del ejemplo de la biblioteca.

Una pieza nueva. const val declara una constante cuyo valor se conoce al compilar y no cambia: const val URL_LIGA = "…" es una val que además queda fijada de antemano.

Intenta escribirlo tú. En LigaApi.kt:

  1. Dos DTO: ArbitroDto (el id, el name y la address) y DireccionDto (solo la city). Solo declaras lo que Marcador va a usar; la dirección es un objeto dentro del árbitro, como el autor dentro del libro.
  2. La interfaz LigaApi con una función, arbitros(), que hace GET a la ruta users y devuelve la lista de ArbitroDto.
  3. La constante URL_LIGA con la base https://jsonplaceholder.typicode.com/.
  4. crearOkHttp(), que construye el motor con 10 segundos de timeout de conexión y de lectura.
  5. crearLigaApi(cliente, url), que arma Retrofit con ese motor, la URL (por defecto URL_LIGA) y el traductor de JSON, y devuelve la LigaApi.

Debes ver que la app compila y se ve igual que antes.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
app/src/main/java/com/sebastiannarvaez/marcador/core/network/LigaApi.kt
package com.sebastiannarvaez.marcador.core.network
 
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.GET
import java.util.concurrent.TimeUnit
 
// LA API DE LA LIGA, CON RETROFIT
//
// El servidor es JSONPlaceholder, un servicio de pruebas gratuito: sus «usuarios» hacen
// aqui de arbitros de la liga. Los nombres son inventados y en ingles.
//
// DTO (Data Transfer Object) = la forma del JSON TAL COMO LLEGA. No es el modelo de la app:
// los campos se llaman como en el servidor (`name`, `address`) y solo se declaran los que
// se usan. El resto del JSON (email, phone, company...) se ignora con `ignoreUnknownKeys`.
// El repositorio traduce el DTO a `Arbitro` (dominio) en un solo sitio.
@Serializable
data class ArbitroDto(val id: Int, val name: String, val address: DireccionDto)
 
@Serializable
data class DireccionDto(val city: String)
 
// La interfaz: cada funcion es UNA peticion. Retrofit escribe la clase que la cumple.
// `suspend`: la llamada espera fuera del hilo principal (OkHttp tiene sus propios hilos),
// asi que no hace falta `withContext(Dispatchers.IO)`.
interface LigaApi {
    // GET https://jsonplaceholder.typicode.com/users
    @GET("users")
    suspend fun arbitros(): List<ArbitroDto>
}
 
// La URL base termina en «/»: Retrofit le pega la ruta de cada funcion («users»).
const val URL_LIGA = "https://jsonplaceholder.typicode.com/"
 
// OkHttp es el motor: abre las conexiones y espera las respuestas. 10 s de timeout: si el
// servidor no contesta en ese tiempo, la llamada falla con una IOException.
fun crearOkHttp(): OkHttpClient = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(10, TimeUnit.SECONDS)
    .build()
 
fun crearLigaApi(cliente: OkHttpClient, url: String = URL_LIGA): LigaApi {
    val json = Json { ignoreUnknownKeys = true }
    return Retrofit.Builder()
        .baseUrl(url)
        .client(cliente)
        .addConverterFactory(json.asConverterFactory("application/json; charset=UTF-8".toMediaType()))
        .build()
        .create(LigaApi::class.java)
}

Qué hace cada pieza nueva:

  • crearOkHttp() fija un timeout de 10 s: si el servidor no contesta, la llamada falla con una IOException en vez de esperar sin fin.
  • crearLigaApi recibe el motor de fuera en vez de construirlo dentro. Así más adelante habrá un único motor para toda la app, y los tests podrán darle otra dirección.
  • Nada llama todavía a LigaApi: por eso la app se ve igual. Compilar es la prueba de que las piezas encajan. Tampoco las usa aún Hilt: en Red en Marcador: la lista de árbitros se las darás en un módulo de red, y será él quien las llame.

Punto de control

  • Sync termina sin rojo y ./gradlew :app:assembleGratisDebug termina en BUILD SUCCESSFUL.
  • El comando de dependencias muestra retrofit:3.0.0, converter-kotlinx-serialization:3.0.0 y okhttp:5.3.2.
  • En la pestaña Merged Manifest de AndroidManifest.xml aparece android.permission.INTERNET.
  • El ejercicio enseña las tres tareas en el emulador, y «Sin conexión» con el modo avión.
  • Marcador se ve igual que antes y ./gradlew :app:testGratisDebugUnitTest pasa con 10 tests con tus ejercicios (si hiciste el del temporizador) y 7 sin ellos: los ejercicios suman sus propios tests y esta lección no añade ninguno.

La lección siguiente pone la primera pantalla encima de esta interfaz: Red en Marcador: la lista de árbitros.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Cómo se reparten el trabajo OkHttp y Retrofit?

  2. 2.En @GET("libros") suspend fun libros(@Query("limite") limite: Int): List<LibroDto>, ¿qué URL se pide con libros(3)?

  3. 3.Pones connectTimeout(10, TimeUnit.SECONDS) y readTimeout(10, TimeUnit.SECONDS). ¿Qué limita cada uno?

  4. 4.OkHttp 5 ya trae el permiso de internet en su manifiesto, y la app compila sin la línea INTERNET. ¿Por qué se escribe igualmente?

  5. 5.Ejecutas tu ejercicio con el modo avión y la pantalla dice «Sin conexión». ¿Qué ha ocurrido por debajo?

Faltan 5 de 5