Saltar al contenido
K
Jetpack y arquitecturaConcepto30 min de lectura · 8 min de máquina Intermedio

Red: HTTP y JSON

Al terminar: Entender qué es una petición HTTP, su respuesta y el JSON, y separar el DTO del modelo.

Al terminar esta lección sabrás qué ocurre cuando una app «pide algo a internet»: qué viaja en una petición, qué trae la respuesta, qué significa cada código de estado y por qué la app no usa el JSON tal cual, sino una clase aparte llamada DTO. No instalas nada ni tocas Marcador: es la lección de ideas. En la siguiente instalarás Retrofit y harás la primera llamada de verdad.

Importa porque casi toda app real depende de datos que no lleva dentro: una lista de árbitros, una crónica que se publica, un usuario que inicia sesión. Todo eso viaja por HTTP, y si no sabes leer una petición y su respuesta, cualquier fallo de red parece magia. Temas: cliente y servidor, petición y respuesta, método, código de estado, timeout, JSON y DTO frente a modelo.

Un aviso sobre los datos. Marcador usa JSONPlaceholder, un servicio de pruebas gratuito: los «árbitros de la liga» son sus usuarios de muestra, con nombres inventados y en inglés, y lo que envíes no se guarda de verdad. Está hecho para practicar.

Qué es pedir algo a un servidor

Los datos de los árbitros no están dentro de la app. Los tiene un servidor: un ordenador de otra persona, siempre encendido, que contesta preguntas. Quien pregunta es el cliente: en este curso, tu app. Un servidor pensado para que lo consulten programas, y no personas, es una API. Las reglas con las que cliente y servidor conversan se llaman HTTP.

Cada parte tiene nombre. Todas aparecen en esta petición, con una dirección de verdad: https://jsonplaceholder.typicode.com/todos?_limit=3. Puedes pegarla en el navegador y ver la respuesta.

ParteQué esEn el ejemplo
URLLa dirección: a quién y qué le pideshttps://jsonplaceholder.typicode.com/todos?_limit=3
ProtocoloCómo se habla; la «s» indica que va cifradohttps://
ServidorQuién contestajsonplaceholder.typicode.com
RutaQué cosa quieres/todos
ParámetrosPares clave=valor tras el ?, separados por &_limit=3: «dame solo 3»
MétodoQué quieres hacer. GET es «dame»; POST es «toma, guarda esto» (lo verás en POST y errores de red)GET
Código de estadoEl sello del sobre: un número200
CabecerasNotas pegadas al sobre; la que más importa es Content-Type (en qué formato viene lo de dentro)application/json
CuerpoLo de dentro del sobre. Aquí, JSONla lista de tareas

Los códigos de estado se agrupan por su primera cifra:

  • 200 (y los demás 2xx): fue bien. 201 es «creado»: lo verás al publicar.
  • 404: «eso no existe». La petición no puede cumplirse, y repetirla da lo mismo.
  • 500, 503: el servidor falló. Ahí a veces sí merece la pena reintentar más tarde.

Y un último concepto: el timeout, el tiempo máximo que esperas antes de rendirte. Sin él, una petición sin contestar dejaría la pantalla esperando para siempre. Hay dos: cuánto esperas a que se abra la conexión y cuánto esperas a que lleguen los datos una vez abierta. Los pondrás en la lección siguiente.

Esto es lo que contestó JSONPlaceholder a dos peticiones reales (7 de octubre de 2026, resumido; el servidor da el cuerpo con más campos de los que enseño):

GET https://jsonplaceholder.typicode.com/users/1
→ 200, content-type: application/json; charset=utf-8
{"id":1,"name":"Leanne Graham","username":"Bret","email":"Sincere@april.biz",
 "address":{"street":"Kulas Light","city":"Gwenborough", ...},
 "phone":"1-770-736-8031 x56442","website":"hildegard.org","company":{...}}
 
GET https://jsonplaceholder.typicode.com/users/99
→ 404 {}

La primera trae el sello 200 y un JSON con muchos campos. La segunda pide un usuario que no existe: el sello es 404 y el cuerpo, vacío.

El JSON no es tu modelo: DTO

Mira el primer JSON de arriba. Tiene name, address y city en inglés, con la ciudad dentro de la dirección (un objeto dentro de otro) como lo decidió el servidor, y además phone, website, company… que Marcador no necesita. Si pintaras ese JSON tal cual, tu app dependería de cómo lo escribe el servidor: el día que cambie un nombre de campo, se rompen las pantallas.

La solución son dos clases con dos trabajos distintos:

  • El DTO (Data Transfer Object, «objeto de transferencia de datos») tiene la forma del JSON tal como llega: mismos nombres, mismo anidado (un objeto dentro de otro es un DTO dentro de otro DTO) y solo los campos que usas.
  • El modelo de dominio tiene la forma que le conviene a tu app: nombres en español y solo lo que se pinta.

Entre las dos hay una pequeña función que traduce, escrita en un único sitio. Si el servidor cambia, tocas el DTO y esa función; el resto de la app ni se entera. Un ejemplo con datos ajenos a Marcador, una biblioteca pública, y lo que imprime (se ejecuta tal cual en el Kotlin Playground):

ejemplo mínimo: del DTO al modelo (con comentarios)
// Los DTO: el libro tal como lo manda el servidor (nombres en inglés).
// El autor es un objeto DENTRO del libro, así que es otro DTO dentro del primero.
data class LibroDto(val id: Int, val title: String, val pages: Int, val author: AutorDto)
data class AutorDto(val name: String)
 
// El modelo: el libro como lo usa mi pantalla (en español, sin el id, con el autor ya aplanado)
data class Libro(val titulo: String, val paginas: Int, val autor: String)
 
// La traducción, en un solo sitio: toma el DTO y construye el modelo
fun LibroDto.aDominio() = Libro(title, pages, author.name)
 
fun main() {
    // Lo que «llegó» del servidor (aquí escrito a mano, sin red)
    val llegaDelServidor = listOf(
        LibroDto(1, "Dune", 612, AutorDto("Frank Herbert")),
        LibroDto(2, "Emma", 480, AutorDto("Jane Austen")),
    )
    // La pantalla nunca ve el DTO: solo el resultado de traducir
    val paraLaPantalla = llegaDelServidor.map { it.aDominio() }
    paraLaPantalla.forEach { println("${it.titulo} (${it.autor}): ${it.paginas} páginas") }
}
Dune (Frank Herbert): 612 páginas
Emma (Jane Austen): 480 páginas

Reto · Ejercicio: del DTO de una película al modelo

Fuera de Marcador, en el Kotlin Playground.

Un servidor de cine manda cada película con la forma { "id": 1, "title": "Alien", "runtime": 117, "director": { "name": "Ridley Scott" } }, donde runtime son los minutos. Escribe los DTO (PeliculaDto y DirectorDto), el modelo Pelicula con tres campos de texto (titulo, duracion y director) y la función aDominio().

La duración del modelo se escribe para personas: 117 minutos pasan a «1 h 57 min», 105 a «1 h 45 min», y si la película dura menos de una hora se escribe solo «2 min» (sin «0 h»). Pista: la división entera / da las horas completas y % da los minutos que sobran.

En main, traduce estas tres películas e imprime una línea por cada una con el formato «título · duración · director»: Alien (117 min, Ridley Scott), Coco (105 min, Lee Unkrich) y Luxo Jr. (2 min, John Lasseter). La consola debe mostrar:

Alien · 1 h 57 min · Ridley Scott
Coco · 1 h 45 min · Lee Unkrich
Luxo Jr. · 2 min · John Lasseter
Ver solución
solución: la película, del DTO al modelo
data class DirectorDto(val name: String)
data class PeliculaDto(val id: Int, val title: String, val runtime: Int, val director: DirectorDto)
data class Pelicula(val titulo: String, val duracion: String, val director: String)
 
fun PeliculaDto.aDominio(): Pelicula {
    val horas = runtime / 60       // 117 / 60 = 1 (división entera)
    val minutos = runtime % 60     // 117 % 60 = 57 (lo que sobra)
    val duracion = if (horas == 0) "$minutos min" else "$horas h $minutos min"
    return Pelicula(title, duracion, director.name)
}
 
fun main() {
    val llegaDelServidor = listOf(
        PeliculaDto(1, "Alien", 117, DirectorDto("Ridley Scott")),
        PeliculaDto(2, "Coco", 105, DirectorDto("Lee Unkrich")),
        PeliculaDto(3, "Luxo Jr.", 2, DirectorDto("John Lasseter")),
    )
    llegaDelServidor.map { it.aDominio() }.forEach { println("${it.titulo} · ${it.duracion} · ${it.director}") }
}

El DTO guarda runtime en minutos porque así lo manda el servidor; el modelo guarda la duración ya escrita, porque es lo que pinta la pantalla. Toda esa decisión vive en aDominio(): si el servidor pasara a mandar segundos, solo cambiaría esa función.

Paso · Comprueba que entiendes antes de instalar nada

Objetivo. No hay código nuevo en Marcador: este paso es leer una petición sin ayuda. Marcador se queda tal cual.

Mira esta petición y contesta en voz alta, sin volver arriba: GET https://jsonplaceholder.typicode.com/users?_limit=2. ¿Cuál es la ruta? ¿Y el parámetro? ¿Qué sello esperas si todo va bien? ¿Y si el servidor está caído?

Respuestas: la ruta es /users; el parámetro es _limit=2 (pide solo dos usuarios); si va bien, 200; si el servidor se cae, un 5xx (y si ni siquiera hay conexión, no llega ningún sobre: la petición falla sin respuesta, igual que cuando se agota el timeout).

Punto de control

  • Sabes decir qué es el cliente, el servidor, la ruta, un parámetro, el método y el código de estado de una petición.
  • Sabes por qué la app tiene un DTO (la forma del JSON) y un modelo (la forma que usa tu app), y dónde va la función que traduce.
  • El ejercicio de la película imprime las tres líneas del enunciado.
  • Marcador está igual que antes: ./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.

La lección siguiente instala Retrofit y hace la primera llamada real: Retrofit y la primera llamada.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.Una petición a https://jsonplaceholder.typicode.com/todos?_limit=3 con el método GET. ¿Qué es /todos y qué es _limit=3?

  2. 2.El servidor contesta con el código 404 y, más tarde, con un 503. ¿Qué dice cada uno?

  3. 3.¿Por qué la app no pinta directamente la clase con la forma del JSON (el DTO) y usa un modelo de dominio aparte?

  4. 4.En una petición HTTP, ¿quién es el «cliente» y quién el «servidor»?

  5. 5.El servidor manda "runtime": 117 (minutos) y la pantalla quiere mostrar «1 h 57 min». ¿Dónde se escribe esa conversión?

Faltan 5 de 5