Arquitectura limpia en móvil: capas, DI y testeo desde el inicio

Arrancar una app “rápido” suena bien… hasta que llega el sprint 3 y cada cambio cuesta el doble. Pantallas que se rompen por tocar una validación, bugs que aparecen sin patrón, builds que dan miedo y un equipo que empieza a evitar refactors “porque ahora no podemos parar”. En Bolmia lo vemos a menudo en proyectos de Desarrollo de Apps que nacen como MVP, pero sin una base mínima de arquitectura. Y ojo: no hablo de montar un castillo de patrones. Hablo de lo justo para que tu producto pueda crecer sin convertirse en un bloque de cemento.

Arquitectura limpia en móvil es básicamente esto: separar bien responsabilidades, controlar dependencias y preparar el terreno para testear desde el principio. Si lo haces, ganarás velocidad real (no velocidad de “hoy saco features y mañana pago la deuda”). Si no lo haces, tu roadmap lo acaba decidiendo el código, no el negocio.

Lo que suele fallar en móvil (y por qué pasa tan rápido)

En móvil, el caos llega antes porque el contexto es más hostil: cambios de red, ciclo de vida, permisos, estados, navegación, caché, notificaciones, rendimiento, múltiples tamaños de pantalla… y encima deadlines. Cuando no hay límites claros entre capas, esa complejidad se mete en cualquier sitio. Y entonces aparece el patrón tóxico: una pantalla que “solo muestra datos” termina haciendo lógica de negocio, consumiendo endpoints, formateando reglas de precios y guardando cosas localmente. Funciona… hasta que deja de funcionar.

El problema no es que el equipo sea malo. Es que si no defines dónde vive cada cosa, el código va a ir al lugar más rápido: donde ya estás trabajando. Es humano. Estás en el ViewModel/Controller, te falta una validación, la metes ahí. Te falta un cálculo, lo metes ahí. Te falta un retry, lo metes ahí. Y en tres semanas tienes un monstruo que nadie quiere tocar.

Lo que buscamos con arquitectura limpia es cortar eso con una regla simple: cada capa tiene su trabajo y no se mete en el del vecino.

Qué significa “arquitectura limpia” en una app, sin humo y sin dogmas

Cuando la gente oye “Clean Architecture”, algunos piensan en una estructura enorme con mil interfaces. No hace falta. En móvil, “limpio” suele significar dos cosas:

  1. La lógica de negocio no depende de la UI ni de frameworks.
  2. Los datos (API, caché, DB) no se filtran a la UI como si fueran “modelos del negocio”.

La idea clave es la dirección de dependencias: lo importante (reglas del negocio) debe estar protegido de lo circunstancial (frameworks, librerías, UI). Hoy la UI es Compose, mañana puede ser SwiftUI, Flutter o lo que sea. El negocio debería seguir igual.

Y aquí viene lo interesante: cuando lo planteas así, testear deja de ser un castigo, porque ya no necesitas levantar un emulador para comprobar si un descuento se aplica bien. Lo pruebas con un unit test y listo.

Cómo construir una secuencia lógica: del usuario a los datos, pasando por el negocio

Una forma muy práctica de entender capas es seguir un flujo real. Imagina que el usuario abre una pantalla de “suscripción premium” y pulsa “Comprar”.

  • La UI captura el evento (tap) y lo manda a su lógica de presentación (por ejemplo, un ViewModel).
  • El ViewModel no “compra” nada. Solo invoca un caso de uso del dominio: “iniciar compra”.
  • El caso de uso decide reglas: ¿tiene trial? ¿está en un país con impuestos? ¿aplica cupón? ¿hay que validar algo antes?
  • Ese caso de uso llama a un repositorio (interfaz) para ejecutar lo necesario: pedir precio, confirmar compra, guardar estado.
  • La capa de datos implementa ese repositorio y habla con la API, el SDK de pagos, el almacenamiento local, etc.
  • Vuelve el resultado hacia arriba y la UI renderiza el estado: success, error, loading, etc.

Si haces esto, de repente tienes un flujo que se puede testear por partes. La UI se testea como UI, el negocio como negocio, y los datos como datos.

Presentación: donde la app “se ve”, pero no se decide el negocio

La capa de presentación tiene un trabajo muy específico: representar estados y gestionar interacción. Y sí, ahí hay decisiones, pero decisiones de UI: qué mensaje mostrar, qué componente renderizar, cómo manejar un estado vacío. Lo que no debería vivir ahí es la lógica del “si el usuario es premium y el plan es anual entonces…”.

Aquí es donde muchas apps se rompen: convierten el ViewModel en el nuevo “Dios del proyecto”. Todo pasa por ahí. Y claro, si todo pasa por ahí, es imposible testearlo sin montar medio mundo.

Lo sano es esto: el ViewModel coordina. Pide datos a casos de uso, transforma resultados en estados de UI y expone esos estados. Si estás trabajando con arquitectura MVVM en apps, piensa en el ViewModel como el traductor: convierte lenguaje “de negocio” en lenguaje “de interfaz”.

Además, una presentación bien diseñada se apoya muchísimo en un buen diseño UX/UI para apps. ¿Por qué? Porque los estados se vuelven claros. Cuando el UX está difuso, el código se llena de excepciones: “si pasa esto, pero si pasa lo otro, pero si el usuario viene de aquí…”. Si el flujo está bien pensado, la UI tiene menos “ifs” y más estados explícitos. Menos magia, menos bugs.

Dominio: el corazón que no debería saber nada de pantallas ni de APIs

El dominio es donde vive lo que hace que tu app sea tu app: reglas, validaciones, cálculos, decisiones. En el dominio defines casos de uso (“crear pedido”, “aplicar cupón”, “guardar dirección”, “calcular tarifa”, “sincronizar carrito”). Y esos casos de uso deberían ser agnósticos del framework.

Aquí es donde se paga (o se ahorra) el coste futuro. Porque el negocio cambia. Siempre. Hoy el cupón vale 10%, mañana vale 12% para un segmento específico, pasado mañana hay un límite por usuario. Si esa lógica está repartida en pantallas, vas a tener inconsistencias y bugs “raros”. Si está centralizada en dominio, cambias un sitio.

Y aquí entra una idea que a nosotros nos encanta: el dominio no debería necesitar conexión a internet para “pensar”. Puede necesitar repositorios para obtener datos, sí, pero a través de contratos, no de implementaciones concretas.

Datos: repositorios, mapeos y el arte de no ensuciar el resto

La capa de datos es donde se resuelve el “cómo”. Cómo hablas con la API, cómo cacheas, cómo reintentas, cómo mapeas DTOs, cómo guardas local, cómo sincronizas offline. Es una capa valiosa, pero es circunstancial: puede cambiar por proveedor, por endpoints, por nuevas políticas, por mejoras.

Aquí es donde suelen aparecer dos errores típicos:

  1. Dejar que los modelos de API suban tal cual a UI. Resultado: la UI acaba dependiendo de DTOs, y si cambia un endpoint, rompes pantallas.
  2. Meter lógica de negocio dentro del repositorio. Resultado: reglas duplicadas, comportamientos inconsistentes y tests confusos.

Lo ideal es que el repositorio sea el “traductor y coordinador” de fuentes de datos, y que exponga al dominio modelos limpios. Y sí, aquí la integración de APIs en apps es un tema serio: no se trata solo de “hacer el call”, sino de manejar errores, timeouts, estados, y devolver cosas que el dominio pueda usar sin conocer el backend.

Inyección de dependencias: la diferencia entre una app testeable y una que da miedo

Vale, ya separaste capas. Ahora viene el siguiente problema: ¿quién crea qué? Si cada clase crea sus dependencias, vuelves a acoplarlo todo. El clásico: una pantalla construye el repositorio, el repositorio construye el cliente HTTP, el cliente HTTP construye el interceptor… y de repente tu UI está pegada a toda la infraestructura.

La DI (inyección de dependencias) evita esto con una idea simple: las clases reciben dependencias desde fuera. Eso te permite cambiar implementaciones en tests, y te permite que el dominio dependa de interfaces y no de clases concretas.

En este punto, si estás pensando en inyección de dependencias (DI) móvil, no te obsesiones con frameworks. Empieza con DI manual si quieres: constructores, factories, un composition root al inicio. El objetivo es el control, no la herramienta.

Un ejemplo de vida real: quieres testear un caso de uso “calcular tarifa” y ese caso de uso necesita un repositorio de precios. Con DI, en tests le pasas un fake que devuelve precios conocidos. Sin DI, el caso de uso va a “buscar” el repositorio real y te obliga a simular red, base de datos, etc. Y ahí es cuando la gente dice “paso de testear”.

DI manual vs contenedor: la decisión sana para no perder semanas

En Bolmia solemos arrancar con DI manual cuando el proyecto está empezando. ¿Por qué? Porque es transparente, rápido de depurar y obliga a que el equipo entienda el wiring. Cuando el proyecto crece, si el wiring se vuelve pesado, ahí sí consideras un contenedor.

La señal de que necesitas un contenedor no es “vi un tutorial en YouTube”, es que tu wiring se convirtió en una tarea recurrente que te roba foco y genera errores. Pero incluso si usas un contenedor, la idea sigue siendo la misma: el composition root existe (aunque sea implícito) y las capas no se rompen.

Testing desde el inicio: el truco no es “hacer muchos tests”, es hacer los correctos

Hay equipos que se queman porque intentan testear todo desde la UI. Y ahí todo se vuelve frágil: cambia un texto, cambia un selector, se rompen 20 tests. Por eso la secuencia lógica es otra:

  • Primero tests unitarios de dominio (rápidos, baratos).
  • Luego integración de datos (para cosas que sí fallan en producción).
  • Luego UI tests mínimos para flujos core.

Cuando te hablan de testing en apps móviles, piensa “pirámide”: mucho unit, algo de integración, poco de UI. Si inviertes esa pirámide, vas a sufrir.

Unit tests: tu seguro de vida para iterar sin miedo

En dominio testea casos de uso: que apliquen reglas, que validen límites, que manejen errores esperados. La gracia es que estos tests corren rápido y te dan feedback inmediato. Además, te obligan a diseñar bien tus interfaces: si tu caso de uso no se deja testear, probablemente está demasiado acoplado.

Y esto es oro cuando el negocio empieza a pedir cambios “ya”: en lugar de cruzar dedos, ejecutas tests.

Integración de datos: donde se detectan bugs de verdad

Los bugs que duelen suelen vivir en datos: mapeos mal hechos, cachés que no invalidan, errores que no se traducen bien, reintentos infinitos, estados inconsistentes. Los tests de integración te ayudan a atrapar estos problemas antes de producción.

Aquí no necesitas testear el universo. Testea flujos críticos. Por ejemplo: login, fetch de catálogo, guardado offline, sincronización. Con pocos tests bien elegidos, ganas estabilidad real.

UI tests: útiles, pero con dieta estricta

Los UI tests están bien para asegurar que el flujo de compra no se rompe o que el onboarding se completa. Pero si intentas cubrir cada pantalla con UI tests, vas a mantener tests en lugar de producto.

La clave es elegir 3–5 flujos que, si se rompen, te cuestan dinero o reputación. Esos van. El resto se cubre por unit e integración.

Rendimiento, estabilidad y observabilidad: la parte que casi nadie hace al inicio (y luego llora)

Hay un tema que se suele olvidar cuando se habla de arquitectura: medir. Si no mides, tu app decide por ti. Puedes tener una arquitectura preciosa, pero si tu app crashea y no te enteras, estás a ciegas.

Añade desde el inicio:

  • Crash reporting para saber qué se rompe y en qué dispositivos.
  • Métricas de rendimiento (arranque, pantallas lentas, bloqueos).
  • Analítica de eventos para ver dónde abandona el usuario.

No es “marketing”, es producto. Y además te ayuda a priorizar: arreglar lo que afecta al 60% de usuarios vale más que optimizar la pantalla que nadie abre.

Un enfoque práctico: cómo arrancar un proyecto sin montar una “catedral”

Vamos a lo accionable. Si mañana arrancas una app, lo que quieres es equilibrio: base sólida, sin eternizarte.

Empieza por modelar 2–3 flujos críticos (los que van a justificar el MVP). Define casos de uso para esos flujos. Crea repositorios con interfaces claras. Monta DI manual. Y escribe tests unitarios para esas reglas. Con eso ya tienes una base.

Luego creces por repetición: cada feature nueva sigue el mismo camino. UI → caso de uso → repositorio → data source. Si el equipo respeta ese flujo, la arquitectura se mantiene sin “policía de patrones”.

Y aquí entra algo importante: arquitectura también es cultura. Si el equipo no revisa PRs con foco en límites de capas, tarde o temprano alguien meterá lógica en UI “porque era más rápido”. No por maldad: por presión. Por eso conviene tener un criterio claro desde el sprint 1.

Errores típicos que vemos (y cómo evitarlos sin drama)

El error número uno es confundir “arquitectura limpia” con “muchas capas”. No necesitas 17 niveles. Necesitas límites claros. A veces con tres capas y contratos bien definidos es suficiente.

El segundo error es el “dominio anémico”: crear un dominio que no decide nada y dejar reglas en UI o datos. Si el dominio no contiene reglas, ¿para qué existe?

El tercero es el “repositorio omnipotente”: repositorios que hacen de todo, deciden negocio, transforman UI, guardan analytics… y se vuelven in-testables. Un repositorio debería coordinar fuentes de datos, no gobernar el producto.

Y el cuarto, el clásico: dejar los tests para el final. El final nunca llega. Si quieres calidad, el primer test debe aparecer cuando aparece la primera regla importante.

Cierre: una app bien montada te deja vender, iterar y crecer sin miedo

Cuando separas capas, usas DI con sentido y metes tests desde el inicio, la app deja de ser una lotería. Los cambios se vuelven predecibles. El equipo avanza más rápido porque no está apagando fuegos. Y el negocio puede iterar sin que cada mejora sea una amenaza.

Y lo mejor: no necesitas una arquitectura “perfecta”. Necesitas una arquitectura suficientemente clara para que el código no te secuestre el roadmap.

También te puede interesar nuestra guía sobre el futuro de las agencias digitales y por qué Bolmia es más que Marketing.

Preguntas frecuentes

1) ¿Qué beneficios da separar capas en una app móvil?

Te permite cambiar UI o datos sin romper lógica de negocio, reduce deuda técnica y acelera iteraciones porque cada parte tiene responsabilidades claras.

2) ¿Es obligatorio usar un framework de DI?

No. Puedes empezar con DI manual (constructores + factories). Un framework compensa cuando el wiring crece y los módulos/scopes se vuelven complejos.

3) ¿Dónde debería vivir la lógica de negocio?

En dominio, dentro de casos de uso. La UI coordina y muestra estados; datos ejecuta acceso a red/caché/DB, pero no decide reglas de negocio.

4) ¿Qué tests conviene implementar primero?

Unit tests en dominio (rápidos y baratos), luego tests de integración en datos para flujos críticos y, al final, pocos UI tests para lo más sensible (login, compra, etc.).

5) ¿Cómo evito que el ViewModel se convierta en un “mega archivo”?

Mantén el ViewModel como orquestador: llama casos de uso y convierte resultados en estados. Si hay reglas repetidas, muévelas a dominio.