Consumo de API REST y gestión de datos remotos en Android
HTTP, Retrofit, DTO, repositorios, estados de UI, errores y CRUD
Tecnologías: Kotlin, Jetpack Compose, corrutinas, Retrofit, OkHttp y kotlinx.serialization.
Introducción
Una aplicación móvil suele obtener y modificar información alojada fuera del dispositivo: catálogos, perfiles, pedidos, publicaciones o registros. Al acceder a un servidor aparecen situaciones que no existen cuando los datos se encuentran en una lista en memoria. Las peticiones tardan, la conexión puede fallar, las respuestas pueden ser rechazadas y otros clientes pueden modificar los mismos registros.
Esta guía explica cómo consumir una API REST desde Android manteniendo separadas las responsabilidades de presentación, gestión del estado, acceso a datos y dominio. Los ejemplos usan un catálogo de productos y comentarios, pero los principios se aplican a otras aplicaciones que consulten o modifiquen información mediante HTTP.
Al finalizar el estudio, el estudiante deberá poder interpretar contratos HTTP, definir endpoints en Retrofit, transformar respuestas JSON en modelos de dominio, gestionar operaciones asíncronas, representar estados de carga y error y justificar el comportamiento de la interfaz frente a distintas respuestas del servidor.
Lo imprescindible para el examen
Interpretar métodos y respuestas HTTP; explicar Retrofit, DTO y repositorio; modelar estados de carga y error; decidir cuándo una escritura admite reintento seguro.
Se evalúa la capacidad de explicar, interpretar fragmentos y justificar decisiones. Las versiones puntuales de librerías, URLs, nombres de proyecto y comandos accesorios quedan como contexto de consulta.
Criterios transversales para interpretar esta guía
Principio transferible. Conviene distinguir la responsabilidad de una tecnología de la forma concreta en que se utilizó en la práctica. Un ejemplo de ViewModel, Retrofit, Hilt o WebSocket ilustra una solución posible; su configuración exacta no es una regla universal.
Decisión del proyecto. Los nombres, rutas, versiones, intervalos, tamaños y elecciones de librerías del caso original sirven para interpretar el código. Salvo indicación expresa, no se exige memorizarlos para el examen.
Alcance del código. Los fragmentos son recursos didácticos; pueden omitir imports, dependencias, permisos, detalles de manejo de errores o infraestructura. Comprender un fragmento no equivale a comprobar que compila como proyecto independiente.
Referencias cruzadas. La síntesis transversal de la colección reúne las distinciones entre estado y evento, persistencia, reintentos, idempotencia y límites de las pruebas.
1. Cliente, servidor y API REST
Una API ofrece operaciones mediante las cuales un cliente interactúa con un sistema. En una aplicación Android, el cliente envía peticiones HTTP y el servidor devuelve respuestas con un código de estado y, cuando corresponde, un cuerpo de datos.
Pantalla Compose
│ evento del usuario
▼
ViewModel
│ corrutina
▼
Repositorio
│
▼
Retrofit / OkHttp ─── HTTP ───► Servidor / API
▲ │
└──────── respuesta ────────┘
La API define un contrato: rutas, métodos, parámetros, formato de los cuerpos, respuestas y posibles errores. Es importante conocer ese contrato antes de escribir las funciones que lo consumen.
1.1 Métodos HTTP habituales
| Método | Propósito frecuente | Ejemplo |
|---|---|---|
GET |
Consultar recursos | GET /products |
POST |
Crear un recurso | POST /comments |
PATCH |
Actualizar parcialmente | PATCH /comments/12 |
PUT |
Reemplazar la representación de un recurso según el contrato | PUT /products/12 |
DELETE |
Solicitar la eliminación | DELETE /comments/12 |
CRUD significa crear, leer, actualizar y eliminar. Los métodos HTTP permiten implementar estas operaciones, pero sus detalles concretos dependen del contrato de cada API.
1.2 Códigos de respuesta
| Código | Interpretación HTTP | Tratamiento típico |
|---|---|---|
200 OK |
Petición satisfactoria | Procesar el cuerpo recibido |
201 Created |
Recurso creado | Utilizar los datos o la ubicación del nuevo recurso |
204 No Content |
Éxito sin cuerpo | Confirmar la operación sin intentar deserializar un objeto obligatorio |
400 Bad Request |
Petición incorrecta | Revisar los datos enviados |
401 Unauthorized |
Faltan credenciales válidas de autenticación | Solicitar o renovar autenticación según el sistema |
403 Forbidden |
No existe permiso suficiente | Informar la restricción |
404 Not Found |
Recurso no encontrado | Mostrar un estado apropiado o actualizar la información |
409 Conflict |
Conflicto con el estado actual | Resolver según reglas del dominio |
422 Unprocessable Content |
El servidor no puede procesar el contenido semánticamente | Mostrar o asociar errores de validación |
500 / 503 |
Error del servidor o servicio no disponible | Indicar el problema y considerar un reintento apropiado |
Importante: un 403 es una respuesta del servidor, no necesariamente un fallo de programación en Android. Asimismo, una petición que falla por falta de red normalmente no tiene un código HTTP de respuesta: no se obtuvo una respuesta HTTP utilizable.
1.3 Ruta, parámetro y cuerpo
GET /comments?productId=8
└─────────── parámetro de consulta
PATCH /comments/12
└── segmento de ruta
POST /comments
Content-Type: application/json
{"productId":8,"rating":5,"text":"Buen producto"}
└────────────────── cuerpo JSON ──────────────────┘
2. Retrofit: describir el contrato HTTP
Retrofit permite declarar endpoints como métodos de una interfaz Kotlin. Las anotaciones indican el método HTTP y cómo se construye la petición.
import retrofit2.Response
import retrofit2.http.*
interface CatalogApi {
@GET("products")
suspend fun getProducts(): List<ProductDto>
@GET("products/{id}")
suspend fun getProduct(
@Path("id") id: Int
): ProductDto
@GET("comments")
suspend fun getComments(
@Query("productId") productId: Int
): List<CommentDto>
@POST("comments")
suspend fun createComment(
@Body request: CreateCommentRequest
): CommentDto
@PATCH("comments/{id}")
suspend fun updateComment(
@Path("id") id: Int,
@Body request: UpdateCommentRequest
): CommentDto
@DELETE("comments/{id}")
suspend fun deleteComment(
@Path("id") id: Int
): Response<Unit>
}
@GET,@POST,@PATCHy@DELETEdeclaran el método y la ruta relativa.@Pathinserta un valor en un segmento de la ruta.@Queryincorpora parámetros a la URL.@Bodyenvía el objeto serializado como cuerpo de la petición.Response<T>ofrece acceso al código, encabezados y cuerpo de la respuesta.suspendpermite invocar el método desde una corrutina sin bloquear el hilo principal durante la espera de la operación de red gestionada por Retrofit.
Una función marcada suspend no ejecuta automáticamente el trabajo en paralelo. El paralelismo o la concurrencia dependen de cómo se organicen las corrutinas. Tampoco todas las funciones suspend son necesariamente seguras para invocarse desde el hilo principal: una implementación propia todavía podría ejecutar trabajo bloqueante.
2.1 Dos estilos de respuesta
Con un método que devuelve directamente ProductDto, Retrofit entrega el objeto en caso de éxito y, con el comportamiento habitual de su adaptador suspendido, lanza HttpException ante una respuesta HTTP no exitosa.
@GET("products/{id}")
suspend fun getProduct(@Path("id") id: Int): ProductDto
Con Response<ProductDto> se puede inspeccionar el código, incluso cuando no sea exitoso:
@GET("products/{id}")
suspend fun getProductResponse(
@Path("id") id: Int
): Response<ProductDto>
No existe una obligación universal de usar uno de los dos. La elección depende de cómo se quiera centralizar el manejo de errores y de si es necesario inspeccionar la respuesta completa. En el segundo caso, se debe verificar isSuccessful o code() explícitamente.
3. Configuración de Retrofit y OkHttp
Retrofit necesita una URL base, un cliente HTTP y, cuando se trabaja con objetos JSON, un convertidor compatible con el mecanismo de serialización elegido.
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
private val json = Json {
ignoreUnknownKeys = true
}
private val httpClient = OkHttpClient.Builder().build()
val api: CatalogApi = Retrofit.Builder()
.baseUrl("https://api.example.com/v1/")
.client(httpClient)
.addConverterFactory(
json.asConverterFactory("application/json".toMediaType())
)
.build()
.create(CatalogApi::class.java)
La URL utiliza un dominio ficticio; para ejecutar el código debe sustituirse por una API disponible. Las dependencias y sus versiones deben configurarse en Gradle de forma compatible con el proyecto. Para acceder a la red, el manifiesto requiere:
<uses-permission android:name="android.permission.INTERNET" />
3.1 URL base y rutas relativas
La URL base de Retrofit debe terminar con /. Además, hay que distinguir las rutas relativas con y sin barra inicial:
.baseUrl("https://api.example.com/v1/")
@GET("products") // https://api.example.com/v1/products
@GET("/products") // https://api.example.com/products
Una ruta que comienza con / no es inválida por sí misma; cambia la forma en que se resuelve contra la URL base. Puede provocar un 404 si elimina accidentalmente el prefijo /v1/ que la API necesita.
3.2 Serialización y cambios en el servidor
ignoreUnknownKeys = true permite ignorar propiedades JSON que no aparecen declaradas en el DTO. Esto ayuda cuando la API añade campos nuevos, pero no corrige tipos incompatibles, propiedades obligatorias ausentes ni JSON malformado.
4. DTO, dominio y mapeadores
Un DTO (Data Transfer Object) representa los datos que llegan o salen a través del contrato de una API. Un modelo de dominio describe los conceptos y reglas que utiliza la aplicación.
import kotlinx.serialization.Serializable
@Serializable
data class ProductDto(
val id: Int,
val name: String,
val priceCents: Long,
val available: Boolean
)
// Modelo de dominio: no depende de Retrofit ni del formato JSON.
data class Product(
val id: Int,
val name: String,
val priceCents: Long,
val available: Boolean
) {
val canPurchase: Boolean get() = available && priceCents >= 0
}
Aunque coincidan algunos campos, las clases responden a responsabilidades diferentes. Si el servidor cambia priceCents por otro nombre, el cambio se atiende en la capa de transporte y en el mapeador, en lugar de propagarse automáticamente a toda la aplicación.
Un mapeador realiza la transformación:
fun ProductDto.toDomain(): Product = Product(
id = id,
name = name,
priceCents = priceCents,
available = available
)
Las funciones de extensión, como toDomain(), se importan desde el paquete donde están definidas. Si falta el import, puede aparecer Unresolved reference.
La separación DTO/dominio es una decisión arquitectónica útil cuando existe una diferencia conceptual o se busca aislar el contrato externo. En aplicaciones muy pequeñas pueden coincidir modelos, aunque se acepta entonces un mayor acoplamiento.
4.1 DTO de entrada y de salida
No siempre se envían los mismos campos que se reciben. Es frecuente que el servidor genere el identificador, la fecha de creación o la identidad del autor.
@Serializable
data class CreateCommentRequest(
val productId: Int,
val rating: Int,
val text: String
)
@Serializable
data class CommentDto(
val id: Int,
val productId: Int,
val author: String,
val rating: Int,
val text: String
)
El cliente no debería asumir que puede elegir campos administrados exclusivamente por el servidor.
5. Repositorio: aislar el origen de los datos
El repositorio proporciona operaciones útiles para el resto de la aplicación y encapsula el acceso a la API. Su implementación puede cambiar sin obligar a la interfaz a trabajar directamente con HTTP o JSON.
class ProductRepository(
private val api: CatalogApi
) {
suspend fun getAll(): List<Product> =
api.getProducts().map { it.toDomain() }
suspend fun getById(id: Int): Product =
api.getProduct(id).toDomain()
}
El uso de una dependencia recibida por el constructor facilita sustituir la API real por una implementación de prueba. La UI no debe construir manualmente un Retrofit ni deserializar JSON.
Una consideración importante: cambiar un repositorio en memoria por uno remoto no siempre implica modificar una sola clase. Si sus operaciones pasan a ser suspend, los consumidores deben adaptarse a la asincronía y a los posibles fallos. La separación en capas limita el alcance del cambio, pero no elimina las diferencias entre leer memoria y consultar una red.
6. Corrutinas y operaciones de red
Las llamadas suspendidas de Retrofit se integran con las corrutinas. En Android, una ubicación habitual para iniciar cargas de datos es el ViewModel, utilizando viewModelScope.
class ProductsViewModel(
private val repository: ProductRepository
) : ViewModel() {
var products by mutableStateOf<List<Product>>(emptyList())
private set
fun loadProducts() {
viewModelScope.launch {
products = repository.getAll()
}
}
}
Esta primera versión muestra cómo invocar la operación, pero todavía no representa adecuadamente el proceso completo: una lista vacía podría significar que aún no se han recibido los datos, que el servidor devolvió cero productos o que hubo un fallo no gestionado.
La siguiente sección resuelve ese problema.
7. Estados de carga, éxito y error
Una pantalla que obtiene datos remotos debe representar, como mínimo, tres resultados del proceso de carga:
- Cargando: la operación todavía está pendiente.
- Éxito: se recibieron datos, incluso si la colección está vacía.
- Error: no se completó la operación como se esperaba.
sealed interface LoadState<out T> {
data object Loading : LoadState<Nothing>
data class Success<T>(val data: T) : LoadState<T>
data class Error(val message: String) : LoadState<Nothing>
}
Una interfaz sellada (sealed interface) permite que el compilador compruebe los casos de un when exhaustivo.
7.1 ViewModel con manejo de estados
class ProductsViewModel(
private val repository: ProductRepository
) : ViewModel() {
var state by mutableStateOf<LoadState<List<Product>>>(LoadState.Loading)
private set
fun loadProducts() {
viewModelScope.launch {
state = LoadState.Loading
state = try {
LoadState.Success(repository.getAll())
} catch (e: IOException) {
LoadState.Error("No fue posible conectar con el servidor")
} catch (e: HttpException) {
LoadState.Error("El servidor respondió ${e.code()}")
}
}
}
}
El fragmento presupone los imports de ViewModel, viewModelScope, launch, mutableStateOf, IOException y retrofit2.HttpException. Para proyectos de producción conviene valorar errores de serialización y otros fallos inesperados, sin interceptar accidentalmente la cancelación.
7.2 Representación en Compose
@Composable
fun ProductsRoute(
state: LoadState<List<Product>>,
onRetry: () -> Unit
) {
when (state) {
LoadState.Loading -> CircularProgressIndicator()
is LoadState.Error -> Column {
Text(state.message)
Button(onClick = onRetry) { Text("Reintentar") }
}
is LoadState.Success -> {
if (state.data.isEmpty()) {
Text("No hay productos disponibles")
} else {
ProductList(products = state.data)
}
}
}
}
ProductList representa un componente de listado que se implementaría por separado. Al recibir datos y eventos, puede mostrarse en una @Preview sin conexión de red.
El when no tiene que vivir obligatoriamente dentro del NavHost. Puede ubicarse en un Composable contenedor o de ruta (ProductsRoute), mientras que los componentes de contenido (ProductList) permanecen independientes de la red y del ViewModel.
7.3 Estado vacío y estado de error
Una lista vacía recibida correctamente es un caso de éxito sin datos. No debe confundirse con una petición pendiente ni con un error. Estos tres escenarios suelen necesitar mensajes y acciones diferentes.
8. Excepciones de red y errores HTTP
No todos los problemas de red son iguales:
| Situación | Tipo o respuesta habitual | Interpretación |
|---|---|---|
| Sin conectividad, DNS o problemas de transporte | IOException y subtipos |
No fue posible completar el intercambio de red |
Servidor devuelve 403 |
HttpException, si el método devuelve directamente el cuerpo |
Existe respuesta HTTP, pero la operación fue rechazada |
Servidor devuelve 422 |
HttpException, bajo esa misma modalidad |
El servidor rechazó la información enviada |
| JSON incompatible con el DTO | Excepción de deserialización | La respuesta no pudo convertirse al modelo esperado |
| ViewModel destruido durante una carga | Cancelación de corrutina | El trabajo deja de ser necesario |
IOException no significa siempre que el servidor nunca fue contactado: también puede aparecer cuando se pierde una conexión durante una transferencia.
Una respuesta Response<T> requiere manejar códigos no exitosos manualmente. No se debe asumir que ese tipo de retorno siempre produce HttpException para respuestas 4xx o 5xx.
8.1 Cancelación cooperativa
Las corrutinas utilizan CancellationException para comunicar cancelación. Si se captura una excepción demasiado amplia, la cancelación podría quedar absorbida:
try {
repository.getAll()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
// Registrar o transformar un error inesperado.
}
Cuando se usa catch (e: Exception), hay que preservar explícitamente la cancelación. Preferir excepciones específicas hace más claro el tratamiento del error.
9. Operaciones de escritura y validación
Crear, editar o borrar información requiere representar algo más que el estado inicial de una pantalla: puede existir una operación en curso, un resultado exitoso y un fallo al guardar.
data class EditorUiState(
val rating: Int = 0,
val text: String = "",
val saving: Boolean = false,
val errorMessage: String? = null
) {
val canSubmit: Boolean
get() = rating in 1..5 && text.trim().length >= 10 && !saving
}
Con saving = true, el botón puede deshabilitarse y mostrar un indicador. Esto reduce envíos repetidos, aunque una garantía estricta contra duplicados también puede requerir protección en la lógica de la aplicación y mecanismos del servidor, como claves de idempotencia.
9.1 Guardar y confirmar después de la respuesta
fun submit(productId: Int, onSuccess: () -> Unit) {
if (!uiState.canSubmit) return
viewModelScope.launch {
uiState = uiState.copy(saving = true, errorMessage = null)
try {
repository.createComment(
productId,
uiState.rating,
uiState.text
)
uiState = uiState.copy(saving = false)
onSuccess()
} catch (e: IOException) {
uiState = uiState.copy(
saving = false,
errorMessage = "No se pudo guardar. Revisa la conexión"
)
} catch (e: HttpException) {
uiState = uiState.copy(
saving = false,
errorMessage = "El servidor rechazó la operación (${e.code()})"
)
}
}
}
Contexto del fragmento: pertenece a un ViewModel con uiState y a un repositorio que implementa createComment; se muestra para analizar el orden de las operaciones. Una alternativa arquitectónica es exponer un evento de éxito y dejar que la capa de navegación lo observe, evitando pasar callbacks de navegación hacia el ViewModel.
La navegación, el mensaje de confirmación o la actualización visual deben ocurrir solo cuando exista una confirmación apropiada del resultado, salvo que se haya diseñado explícitamente una estrategia de actualización optimista.
9.2 Validación del cliente y del servidor
La validación del cliente mejora la interacción: identifica errores sin necesidad de enviar una petición. La validación del servidor protege las reglas de negocio y la integridad de la información porque un cliente puede modificarse o evitarse por completo.
Por ejemplo, el cliente puede impedir un comentario de menos de diez caracteres; aun así, el servidor debe comprobar ese límite cuando sea una regla del sistema. Una respuesta 422 puede servir para mostrar al usuario el campo que debe corregir.
10. Actualización parcial y eliminación
Con PATCH suelen enviarse únicamente los campos que se desea modificar. Hay que distinguir campo omitido de campo enviado con valor null: su interpretación depende de la API y de la configuración del serializador.
@Serializable
data class UpdateCommentRequest(
val rating: Int? = null,
val text: String? = null
)
Este DTO puede utilizarse para una actualización parcial si el contrato y la serialización permiten omitir los campos no modificados. Declararlos como anulables no garantiza, por sí solo, que nunca viajarán en el JSON.
Para borrar:
suspend fun deleteComment(id: Int): Boolean {
val response = api.deleteComment(id)
return when (response.code()) {
204 -> true
403 -> false
else -> throw HttpException(response)
}
}
Este método reproduce una decisión concreta de contrato: interpreta 204 como éxito y 403 como falta de permiso. En otra API podría haber otros códigos exitosos; no es una regla universal de DELETE.
La interfaz puede ocultar las acciones no autorizadas, pero la autorización efectiva siempre corresponde al servidor. Ocultar un botón no impide que se construya una petición HTTP manualmente.
11. Encabezados, interceptores y seguridad
OkHttp permite agregar lógica común a varias peticiones por medio de interceptores. Por ejemplo:
val identityInterceptor = Interceptor { chain ->
val request = chain.request().newBuilder()
.header("X-Client-Version", "1.0")
.build()
chain.proceed(request)
}
val client = OkHttpClient.Builder()
.addInterceptor(identityInterceptor)
.build()
Esto evita repetir encabezados en cada función de Retrofit. Si una API utiliza tokens de acceso, también puede configurarse un mecanismo para adjuntarlos a las peticiones autorizadas.
Precaución: un encabezado que contiene una matrícula, un identificador editable o una cadena fija no constituye por sí mismo autenticación segura. El servidor no debería confiar en un identificador que cualquier cliente pueda suplantar. En aplicaciones reales se requiere verificar la identidad y aplicar la autorización en el servidor.
Los registros de depuración tampoco deben exponer tokens, contraseñas o información sensible. Un interceptor de logging debe configurarse de acuerdo con el entorno de ejecución.
12. Datos remotos y consistencia de la interfaz
Cuando varios dispositivos usan la misma API, los datos pueden cambiar fuera del dispositivo actual. Después de crear o borrar un registro, la aplicación necesita una estrategia para mantener coherente la información visible:
- Recargar: solicitar nuevamente los datos actualizados al servidor.
- Actualizar el estado local: modificar la representación que ya se tiene, siempre que exista información suficiente.
- Actualización optimista: reflejar el cambio antes de la confirmación y revertirlo si falla.
- Caché: mantener una copia local para mejorar el tiempo de respuesta o el uso sin conexión, definiendo cuándo deja de ser válida.
La elección depende de los requisitos. Recargar es simple y facilita recuperar la verdad del servidor, pero aumenta peticiones; la actualización optimista puede mejorar la percepción de rapidez, aunque exige tratamiento de errores y conflictos.
Un LaunchedEffect(id) puede iniciar una carga cuando un destino entra en composición o cuando cambia su clave, pero no garantiza por sí solo recargar cada vez que se regresa si el destino permanece compuesto o se restaura sin cambiar esa clave. La estrategia debe considerar el ciclo de vida de navegación y la necesidad real de actualización.
Conexión transversal: alcance y decisiones
Reintentos, errores y operaciones idempotentes
Una lectura GET generalmente admite repetición; una escritura POST puede crear duplicados si el servidor la procesó pero la respuesta se perdió. Deshabilitar el botón impide algunos dobles toques, pero no evita el reenvío desde otra instancia ni resuelve una respuesta incierta. Una clave de idempotencia procesada por el servidor puede hacer que reintentos de la misma operación compartan resultado. La estrategia depende del contrato concreto de la API.
Un 401 requiere distinguir credenciales erróneas, acceso expirado y sesión revocada; 403 describe una operación no autorizada y repetirla sin cambiar las condiciones suele ser inútil. 429 y 503 pueden requerir espera; IOException puede ser transitorio; la cancelación debe propagarse. Véase la matriz en sintesis-transversal-android-02-12.md.
13. Errores frecuentes y diagnóstico
| Síntoma | Posible causa | Qué revisar |
|---|---|---|
| Error de permiso al conectarse | Falta INTERNET |
AndroidManifest.xml |
baseUrl must end in / |
URL base sin barra final | Configuración del cliente Retrofit |
404 en todos los endpoints |
Ruta incorrecta o prefijo perdido | URL base y uso de / inicial |
| Error de serialización | JSON y DTO incompatibles | Nombres, tipos y propiedades obligatorias |
Unresolved reference: toDomain |
Falta importar un mapeador | Paquete y función de extensión |
| La pantalla muestra vacío permanentemente | No se distinguen carga, error y éxito vacío | LoadState y manejo de excepciones |
| El botón crea registros repetidos | Envíos concurrentes o reintentos ambiguos | Estado saving e idempotencia |
| El formulario desaparece tras un error | Se navega antes de la confirmación | Orden de submit y onSuccess |
| Se oculta el botón de borrar, pero la API permite borrado ajeno | Autorización exclusiva en el cliente | Validación de permisos en el servidor |
| El listado queda desactualizado | No existe estrategia de refresco | Recarga, estado local o caché |
| Se ignora una cancelación | Captura demasiado amplia | Tratamiento de CancellationException |
Un 403 cierra la app |
Respuesta HTTP sin gestión de error | Response<T>, HttpException y UI de error |
14. Vocabulario esencial
| Concepto | Definición |
|---|---|
| API REST | Interfaz que expone recursos y operaciones utilizando convenciones de HTTP |
| Endpoint | Combinación de método y ruta para una operación |
| DTO | Objeto destinado al intercambio de datos |
| Serialización | Conversión entre objetos y un formato como JSON |
| Retrofit | Biblioteca que convierte definiciones de endpoints en peticiones HTTP |
| OkHttp | Cliente HTTP utilizado por Retrofit |
| Interceptor | Componente que puede examinar o modificar peticiones y respuestas |
| Repositorio | Abstracción de acceso a datos para el resto de la aplicación |
| Mapeador | Transformación entre representaciones, por ejemplo DTO y dominio |
Response<T> |
Representación de una respuesta HTTP con código y otros metadatos |
HttpException |
Excepción de Retrofit asociada con una respuesta HTTP no exitosa en ciertas formas de invocación |
IOException |
Familia de excepciones de entrada/salida que puede incluir fallos de transporte |
UiState / LoadState |
Representación estructurada de los estados de la interfaz |
| CRUD | Crear, consultar, actualizar y eliminar registros |
| Autenticación | Verificación de identidad |
| Autorización | Determinación de las acciones permitidas para una identidad |
| Actualización optimista | Actualización inmediata de la UI con posibilidad de reversión |
15. Autoevaluación
Comprensión conceptual
- ¿Por qué consumir una API obliga a representar más estados que consultar una lista en memoria?
- ¿Qué diferencia hay entre un endpoint, un método HTTP y un código de respuesta?
- ¿Qué función cumplen
@Path,@Queryy@Body? - ¿Por qué conviene separar DTO y dominio? ¿Cuándo podría aceptarse utilizar un único modelo?
- ¿Qué responsabilidad tiene el repositorio frente a la interfaz Retrofit?
- ¿Qué diferencia existe entre devolver
ProductDtoy devolverResponse<ProductDto>? - ¿Por qué
suspendno significa ejecución paralela ni garantiza por sí solo ausencia de bloqueo? - ¿Por qué un cliente no puede garantizar por sí mismo la autorización de las operaciones?
Interpretación de código
- Con URL base
https://api.example.com/v1/, ¿a qué URL apunta@GET("/products")y a cuál@GET("products")? - Analiza
Json { ignoreUnknownKeys = true }. ¿Qué cambio de la API tolera y cuál podría seguir provocando un error? - ¿Qué ocurriría si
statefuese únicamenteemptyList()mientras la carga está pendiente y la petición falla? - ¿Qué riesgo tiene invocar el callback
onSuccess()inmediatamente después de lanzar una corrutina, fuera de su bloque de confirmación? - En un
PATCH, ¿es equivalente omitirtexta incluir"text": null? Justifica. - ¿Por qué el método
deleteComment()del ejemplo utilizaResponse<Unit>?
Aplicación y diagnóstico
- Un catálogo muestra una lista vacía cuando el dispositivo está sin conexión. Propón un modelo de estado y explica qué debería visualizar el usuario.
- Después de crear un registro, la app regresa al listado, pero el nuevo registro no aparece. Menciona dos estrategias de solución y una diferencia entre ellas.
- Una API devuelve
403cuando un usuario intenta modificar el recurso de otra persona. Explica qué corresponde al cliente y qué al servidor. - El servidor agrega
discountRate: 0.1al JSON, pero algunas instalaciones comienzan a fallar. ¿Qué revisarías en el DTO y en el serializador? - Dos toques rápidos en «Guardar» generan dos registros. ¿Qué medidas implementarías en la interfaz y qué protección adicional considerarías en el servidor?
- Se requiere mostrar productos sin conexión utilizando la última respuesta recibida. ¿Qué responsabilidades nuevas se incorporarían a la capa de datos y al estado de pantalla?
16. Respuestas posibles
- Una consulta remota puede tardar, fallar o devolver una lista vacía correctamente. Esos resultados necesitan representaciones diferentes.
- El endpoint identifica una operación por método y ruta; el método describe la intención HTTP; el código expresa el resultado de una petición concreta.
@Pathcompleta segmentos de URL,@Queryagrega parámetros y@Bodyserializa el contenido enviado.- El DTO aísla el contrato externo y el dominio representa conceptos internos. En sistemas pequeños puede aceptarse un solo modelo a cambio de más acoplamiento.
- El repositorio traduce operaciones de acceso a datos a funciones apropiadas para la aplicación, y puede incluir mapeo, coordinación de fuentes y políticas de recuperación.
- El primer estilo entrega datos exitosos o lanza excepciones HTTP en casos no exitosos.
Response<T>permite inspeccionar explícitamente la respuesta y exige revisar sus códigos. suspendpermite puntos de suspensión, pero no lanza otra corrutina automáticamente ni impide que el código implemente una operación bloqueante.- El cliente puede modificarse o sustituirse. El servidor debe comprobar identidad y permisos antes de aceptar una operación.
- La ruta con barra inicial apunta a
https://api.example.com/products; la relativa sin barra apunta ahttps://api.example.com/v1/products. - Tolera propiedades nuevas desconocidas, pero no necesariamente un campo obligatorio faltante o un tipo incompatible.
- Se confundirían ausencia de datos, operación en curso y fallo; el usuario no sabría si esperar, reintentar o concluir que no hay registros.
- El callback se ejecutaría antes de que el servidor confirme, lo que podría cerrar el formulario aunque la operación fallara.
- No necesariamente. Un campo omitido puede significar «sin cambios» y un
nullexplícito puede significar «borrar valor» u otra acción; depende de la API. - Para inspeccionar el código de la respuesta, especialmente cuando el cuerpo está vacío, y aplicar el tratamiento esperado para
204y403. - Usaría una representación
Loading,SuccessyError; sin conexión mostraría un error claro y la posibilidad de reintentar.Success(emptyList())correspondería a una respuesta exitosa vacía. - Recargar del servidor o actualizar el estado local. La recarga obtiene datos recientes a costa de otra petición; el cambio local reduce latencia, pero requiere controlar consistencia.
- El cliente puede ocultar acciones no permitidas y explicar el rechazo. El servidor debe aplicar la autorización y devolver la respuesta apropiada.
- Revisaría si el DTO declara el nuevo campo y si el convertidor está configurado para ignorar claves desconocidas. También descartaría otros cambios incompatibles en el JSON.
- Deshabilitaría la acción durante
saving, evitaría lanzamientos simultáneos y consideraría idempotencia u otra protección del lado servidor ante reintentos y solicitudes duplicadas. - Harían falta un almacenamiento local o caché, una política de actualización, mecanismos de lectura sin conexión y un estado capaz de indicar si se presentan datos guardados y si falló su actualización.
Cierre
El consumo de datos remotos incorpora tiempos de espera, errores, permisos y posibles cambios producidos por otros clientes. Una implementación robusta conserva el contrato HTTP en la capa de red, transforma DTO a modelos adecuados para la aplicación, coordina operaciones desde corrutinas y representa explícitamente los estados que observa el usuario.
Para repasar antes del examen, comprueba que puedes responder sin consultar ejemplos anteriores: ¿qué envía la app?, ¿quién transforma la respuesta?, ¿dónde se administra el estado?, ¿cómo se informa un fallo?, ¿quién garantiza que la operación está autorizada?
Nota editorial: los códigos HTTP, DTO y flujos específicos de la práctica «Sabores en red» se utilizaron como base conceptual. Los nombres del catálogo, las URLs de ejemplo y los fragmentos de implementación de esta guía son ilustrativos; no describen una API real lista para ejecutar. Algunas reglas técnicas del material de práctica se precisaron para distinguir decisiones de un proyecto de comportamientos generales de Retrofit, HTTP y Compose.