ADR-004 — Cliente HTTP del cliente Flutter¶
| Campo | Valor |
|---|---|
| Estado | Aceptado |
| Fecha | 28 de enero de 2026 |
| Decisores | Equipo Custodiam (Marcos Val Sanz, Rodrigo Mulero García) |
Contexto¶
El cliente Flutter habla con custodiam-api por REST sobre HTTPS. Cada feature de la app (voluntarios, servicios, fichajes, inventario) consume varios endpoints y necesita:
- Adjuntar el JWT en
Authorization: Bearer .... - Detectar
401 Unauthorized, intentarrefresh_tokeny reintentar la petición original. - Mapear errores HTTP (
401,403,409,5xx) aFailureespecíficos de la capa data (ADR-014). - Centralizar la
baseUrlpara no diseminarString.fromEnvironmentpor la app (ADR-015).
Hay que decidir qué paquete HTTP usar como base, y cómo organizar el wrapper de transporte.
Decisión¶
Paquete oficial http del equipo Dart (Google), envuelto por una clase ApiClient en lib/infrastructure/network/. El wrapper centraliza:
- Inyección de tokens JWT en cabeceras (
Authorization: Bearer ...). - Manejo de errores HTTP (
401 → refresh + retry,403,5xx). - Refresh coordinado con
AuthService.getValidAccessToken(). baseUrlleída deEnvConfig.- Conversión de respuestas a
Result<T>antes de salir de la capa data.
class VoluntariosRepositoryImpl implements VoluntariosRepository {
final ApiClient _api;
VoluntariosRepositoryImpl(this._api);
@override
Future<Result<List<Voluntario>>> getAll() async {
try {
final json = await _api.get('/voluntarios');
final list = (json['items'] as List)
.map((e) => VoluntarioModel.fromJson(e).toDomain())
.toList();
return Success(list);
} on UnauthenticatedException {
return const Fail(AuthFailure.sessionExpired());
} on ApiException catch (e) {
return Fail(NetworkFailure.serverError(e.statusCode));
}
}
}
Justificación¶
-
Paquete oficial Dart.
httpestá mantenido por el equipo Dart de Google. Tiene la misma garantía de mantenimiento que el propio SDK. No depende de un mantenedor individual ni de una organización externa que pueda perder interés. -
API suficiente para el caso de uso. El proyecto consume REST estándar (
GET,POST,PUT,PATCH,DELETE) con cuerpos JSON, cabeceras estándar y cancelación opcional. Todas las features que el dominio necesita están cubiertas porhttp. -
Sin dependencia transitiva pesada.
httpno arrastrameta,code generation, ni adaptadores extra. La superficie del bundle final es mínima. -
El wrapper
ApiClientcubre lo específico del proyecto. Lo quehttpno hace (interceptors, refresh automático, manejo de errores tipado) lo hace el wrapper en código del propio proyecto, donde reside la lógica de negocio relacionada con auth y errores. Esa lógica no debería vivir en un paquete externo. -
Testabilidad por inyección.
ApiClientrecibe unhttp.Clientpor constructor — los tests pueden inyectar unMockClientdepackage:http/testing.dartsin necesidad de paquetes adicionales de mocking HTTP.
Alternativas evaluadas y descartadas¶
A. dio¶
Cliente HTTP de cuota relevante en Flutter, con interceptors built-in y CancelToken.
- Pros: API más rica que
http, comunidad amplia, soporte de interceptors out-of-the-box. - Contras: dependencia de terceros no mantenida por Google; arrastra peso al bundle; sus interceptors built-in resuelven un problema (refresh, logging) que el wrapper del proyecto cubre con código propio testable.
- Descartado por: dependencia opcional cuando el SDK ya cubre el caso de uso.
B. chopper¶
Cliente con code generation tipo Retrofit (define interfaces, anotaciones, genera el cliente).
- Pros: type-safe, contratos REST declarativos.
- Contras: build_runner extra, anotaciones que el equipo no usa para otras cosas, generación de código que añade fricción en el ciclo de desarrollo (
flutter pub run build_runner watch). - Descartado por: el coste del code-gen no compensa para el tamaño del API.
C. dart:io HttpClient directo¶
API nativa de bajo nivel.
- Pros: cero dependencias.
- Contras: NO funciona en Flutter Web (es API exclusiva del runtime VM/native).
httpenvuelve esta API en plataforma móvil y usaBrowserClienten web, lo que da multiplataforma transparente. - Descartado por: incompatibilidad con Flutter Web.
Implicaciones operativas¶
- Refresh + retry centralizado: si el
ApiClientrecibe un401, invocaAuthService.getValidAccessToken()(que refresca si hace falta) y reintenta la petición original una sola vez. Si el segundo intento también devuelve401, lanzaUnauthenticatedExceptionque el repository convierte aFail(AuthFailure.sessionExpired()). - Tests con
MockClient: cada repository tiene tests que inyectan unMockClientconfigurado para responder ciertos JSON o ciertos status codes, sin levantar servidor real ni mocks adicionales de paquetes. - Logging via
dev.log(ADR-016): cada petición y respuesta se loguea conname: 'API'para facilitar el filtrado en DevTools. - Cabeceras estándar: el wrapper añade
Accept: application/jsonyContent-Type: application/jsoncuando hay body. ElUser-Agentno se sobreescribe (Flutter pone uno con la versión del SDK). - Sin caché HTTP: el wrapper no implementa caché propio. Las features que necesiten persistencia offline usan SQLite local (ADR-005), no caché HTTP.
Referencias¶
- Paquete
httpen pub.dev — documentación oficial. package:http/testing.dart— utilidades de testing oficiales.- ADR-014
Result<T>yFailure— tipo de retorno de los repositories. - ADR-015 EnvConfig —
baseUrlconsumida por el wrapper. - ADR-016 dev.log — logging del wrapper.