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.
| Parte | Qué es | En el ejemplo |
|---|---|---|
| URL | La dirección: a quién y qué le pides | https://jsonplaceholder.typicode.com/todos?_limit=3 |
| Protocolo | Cómo se habla; la «s» indica que va cifrado | https:// |
| Servidor | Quién contesta | jsonplaceholder.typicode.com |
| Ruta | Qué cosa quieres | /todos |
| Parámetros | Pares clave=valor tras el ?, separados por & | _limit=3: «dame solo 3» |
| Método | Qué quieres hacer. GET es «dame»; POST es «toma, guarda esto» (lo verás en POST y errores de red) | GET |
| Código de estado | El sello del sobre: un número | 200 |
| Cabeceras | Notas pegadas al sobre; la que más importa es Content-Type (en qué formato viene lo de dentro) | application/json |
| Cuerpo | Lo de dentro del sobre. Aquí, JSON | la lista de tareas |
Los códigos de estado se agrupan por su primera cifra:
200(y los demás2xx): fue bien.201es «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):
// 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áginasReto · 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 LasseterVer solución
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:testGratisDebugUnitTestpasa 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.Una petición a
https://jsonplaceholder.typicode.com/todos?_limit=3con el métodoGET. ¿Qué es/todosy qué es_limit=3?2.El servidor contesta con el código
404y, más tarde, con un503. ¿Qué dice cada uno?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.En una petición HTTP, ¿quién es el «cliente» y quién el «servidor»?
5.El servidor manda
"runtime": 117(minutos) y la pantalla quiere mostrar «1 h 57 min». ¿Dónde se escribe esa conversión?