Skip to content

VIPER

VIPER es el patrón de arquitectura adoptado en las aplicaciones móviles de GM Transport construidas con Flutter/Dart. Su nombre es el acrónimo de sus cinco componentes: View, Interactor, Presenter, Entity, Router. Cada componente tiene una responsabilidad única y se comunica con los demás a través de interfaces, lo que produce código altamente testeable y modular.

VIPER aplica los principios de Clean Architecture al contexto móvil. La regla de dependencias es la misma: el flujo de dependencias va desde la Vista hacia el Interior, nunca al revés. El Interactor —equivalente al caso de uso— no sabe nada de la interfaz gráfica.

Los cinco componentes

graph LR
    V["View\n(Widget)"] <-->|eventos / estados| P["Presenter\n(Notifier)"]
    P <--> I["Interactor\n(Use Case)"]
    I <--> E["Entity\n(Domain)"]
    P --> R["Router\n(Navigation)"]

View

La View es el Widget de Flutter. Muestra la interfaz de usuario y delega todos los eventos al Presenter. No contiene lógica: no toma decisiones de negocio, no llama directamente a repositorios, no maneja errores de red.

lib/features/solicitud/presentation/solicitud_page.dart
class SolicitudPage extends ConsumerWidget {
final String uuid;
const SolicitudPage({required this.uuid, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(solicitudNotifierProvider(uuid));
return Scaffold(
appBar: AppBar(title: const Text('Solicitud')),
body: switch (state) {
SolicitudLoading() => const Center(child: CircularProgressIndicator()),
SolicitudLoaded(:final solicitud) => SolicitudDetail(solicitud: solicitud),
SolicitudError(:final message) => ErrorView(message: message),
},
);
}
}

Interactor

El Interactor contiene la lógica de negocio de un caso de uso específico. Es equivalente al Use Case de Clean Architecture. Recibe datos de repositorios y aplica las reglas del dominio. Es completamente testeable sin depender de Flutter.

lib/features/solicitud/interactor/conciliar_solicitud_interactor.dart
class ConciliarSolicitudInteractor {
final SolicitudRepository _solicitudRepo;
final ERPGateway _erpGateway;
ConciliarSolicitudInteractor({
required SolicitudRepository solicitudRepo,
required ERPGateway erpGateway,
}) : _solicitudRepo = solicitudRepo,
_erpGateway = erpGateway;
Future<Either<Failure, Conciliacion>> execute(String uuid) async {
final solicitudResult = await _solicitudRepo.findByUUID(uuid);
return solicitudResult.flatMap((solicitud) async {
if (solicitud.estatus != EstatusSolicitud.listaParaConciliar) {
return Left(InvalidTransitionFailure(solicitud.estatus));
}
final facturasResult = await _erpGateway.obtenerFacturas(
rfc: solicitud.rfc,
periodo: solicitud.periodo,
);
return facturasResult.map((facturas) =>
Conciliacion.crear(solicitud: solicitud, facturas: facturas),
);
});
}
}

Presenter

El Presenter coordina la View y el Interactor. Recibe los eventos de la View, los delega al Interactor y transforma el resultado en un estado que la View puede consumir. En Flutter, el Presenter se implementa como un AsyncNotifier de Riverpod.

lib/features/solicitud/presenter/solicitud_notifier.dart
@riverpod
class SolicitudNotifier extends _$SolicitudNotifier {
@override
FutureOr<Solicitud> build(String uuid) async {
final interactor = ref.read(getSolicitudInteractorProvider);
final result = await interactor.execute(uuid);
return result.fold(
(failure) => throw SolicitudException(failure.message),
(solicitud) => solicitud,
);
}
Future<void> conciliar() async {
state = const AsyncLoading();
final interactor = ref.read(conciliarSolicitudInteractorProvider);
final result = await interactor.execute(uuid);
state = result.fold(
(failure) => AsyncError(SolicitudException(failure.message), StackTrace.current),
(conciliacion) => AsyncData(state.value!.copyWith(
estatus: EstatusSolicitud.enProceso,
)),
);
}
}

Entity

Las Entities son las clases de dominio: entidades, value objects y errores de dominio. Son puras Dart —no dependen de Flutter, de Dio, ni de ningún framework. Son inmutables y se prueban con tests de unidad simples.

lib/features/solicitud/entity/solicitud.dart
class Solicitud {
final String uuid;
final String rfc;
final EstatusSolicitud estatus;
final Periodo periodo;
const Solicitud({
required this.uuid,
required this.rfc,
required this.estatus,
required this.periodo,
});
Either<DomainFailure, Solicitud> iniciarProcesamiento() {
if (estatus != EstatusSolicitud.listaParaConciliar) {
return Left(InvalidTransitionFailure(
actual: estatus,
esperado: EstatusSolicitud.enProceso,
));
}
return Right(copyWith(estatus: EstatusSolicitud.enProceso));
}
Solicitud copyWith({EstatusSolicitud? estatus}) {
return Solicitud(
uuid: uuid,
rfc: rfc,
estatus: estatus ?? this.estatus,
periodo: periodo,
);
}
}

Router

El Router gestiona la navegación entre pantallas. Centraliza todas las rutas del módulo y evita que la View tenga acoplamiento directo con otras pantallas.

lib/features/solicitud/router/solicitud_router.dart
class SolicitudRouter {
static void goToDetalle(BuildContext context, String uuid) {
context.push('/solicitudes/$uuid');
}
static void goToConciliacion(BuildContext context, String uuid) {
context.push('/solicitudes/$uuid/conciliacion');
}
static void goBack(BuildContext context) {
context.pop();
}
}

Con GoRouter, el registro de rutas del módulo queda en un archivo centralizado:

lib/features/solicitud/router/solicitud_routes.dart
final solicitudRoutes = [
GoRoute(
path: '/solicitudes/:uuid',
builder: (context, state) => SolicitudPage(
uuid: state.pathParameters['uuid']!,
),
routes: [
GoRoute(
path: 'conciliacion',
builder: (context, state) => ConciliacionPage(
uuid: state.pathParameters['uuid']!,
),
),
],
),
];

Estructura de directorios por módulo

Cada feature de la app sigue la misma estructura:

lib/
├── features/
│ └── solicitud/
│ ├── entity/
│ │ ├── solicitud.dart
│ │ ├── conciliacion.dart
│ │ └── estatus_solicitud.dart
│ ├── interactor/
│ │ ├── get_solicitud_interactor.dart
│ │ └── conciliar_solicitud_interactor.dart
│ ├── presenter/
│ │ └── solicitud_notifier.dart
│ ├── view/
│ │ ├── solicitud_page.dart
│ │ └── widgets/
│ │ └── solicitud_detail.dart
│ └── router/
│ └── solicitud_routes.dart
├── domain/
│ └── solicitud/
│ └── solicitud_repository.dart # Interfaz abstracta
└── infrastructure/
└── solicitud/
└── solicitud_http_repository.dart # Implementación

Testabilidad por componente

ComponenteHerramientaQué se verifica
Entityflutter_test (sin widgets)Reglas de negocio, transiciones de estado
Interactorflutter_test + mocksLógica del caso de uso con repositorios mock
Presenterflutter_test + ProviderContainerEstados generados ante eventos
Viewflutter_test con pumpWidgetWidgets renderizados para cada estado
RouterGoRouter test utilitiesRutas generadas ante cada acción
// Test del Interactor — sin widgets, sin Flutter
void main() {
group('ConciliarSolicitudInteractor', () {
late MockSolicitudRepository mockRepo;
late ConciliarSolicitudInteractor interactor;
setUp(() {
mockRepo = MockSolicitudRepository();
interactor = ConciliarSolicitudInteractor(
solicitudRepo: mockRepo,
erpGateway: MockERPGateway(),
);
});
test('debe retornar Failure si la solicitud está Conciliada', () async {
when(mockRepo.findByUUID('sol-001')).thenAnswer((_) async =>
Right(Solicitud(estatus: EstatusSolicitud.conciliada, ...)),
);
final result = await interactor.execute('sol-001');
expect(result.isLeft(), true);
expect(result.fold((f) => f, (_) => null), isA<InvalidTransitionFailure>());
});
});
}

Relación con Clean Architecture

VIPER es Clean Architecture aplicado al contexto móvil con una nomenclatura adaptada:

Clean ArchitectureVIPER
EntitiesEntity
Use CasesInteractor
Interface AdaptersPresenter
Frameworks & DriversView + Router + Infrastructure

La regla de dependencias es idéntica: Entity no conoce a nadie. Interactor conoce Entity. Presenter conoce Interactor y Entity. View conoce solo el estado que Presenter expone. Router conoce View.

Resumen

  • VIPER divide cada feature en cinco componentes con responsabilidades únicas: View (UI), Interactor (lógica de negocio), Presenter (coordinación y estado), Entity (dominio) y Router (navegación).
  • En Flutter, el Presenter se implementa como AsyncNotifier de Riverpod. La View observa el estado con ref.watch.
  • Las Entities son Dart puro: sin Flutter, sin dependencias externas. Son el componente más testeable.
  • El Interactor recibe interfaces de repositorios (no implementaciones). Esto permite tests sin base de datos ni red.
  • La estructura de directorios es por feature: cada módulo contiene sus cinco capas y es independiente de los demás.

VIPER es un patrón de arquitectura de software que se utiliza en el diseño de aplicaciones móviles para separar las responsabilidades y mejorar la modularidad y la escalabilidad del código. VIPER es un acrónimo que significa View, Interactor, Presenter, Entity, Router y se basa en los principios de la arquitectura limpia y el diseño orientado a objetos.

alt text

El patrón VIPER es una evolución del patrón MVC (Model-View-Controller) y se utiliza principalmente en el desarrollo de aplicaciones móviles para iOS y Android. VIPER se basa en la idea de separar las responsabilidades de las diferentes capas de la aplicación y de utilizar interfaces para comunicar entre ellas. Esto permite que el código sea más modular, reutilizable y fácil de mantener. Ademas de que facilita la escritura de pruebas unitarias y la escalabilidad del código. E incluso permite que diferentes equipos de desarrollo trabajen en paralelo en diferentes partes de la aplicación.

VIPER es una excelente opción para aplicaciones que requieren trabajo en equipo, aplicaciones grandes y complejas, aplicaciones que necesitan ser escalables y aplicaciones que necesitan ser fáciles de mantener y de probar, ademas de ser altamente flexible, modular y reutilizable en cualquier punto de la aplicación.

V - View

La capa de View es la encargada de la interfaz de usuario de la aplicación. En esta capa se encuentran las clases que se encargan de mostrar la información al usuario y de recibir las interacciones del usuario. La capa de View es la capa más externa de la aplicación y es la que se comunica con el usuario.

La capa de View es la encargada de renderizar la interfaz de usuario, de mostrar los datos al usuario y de recibir las interacciones del usuario.

I - Interactor

La capa de Interactor es la encargada de contener la lógica de negocio de la aplicación. En esta capa se encuentran las clases que se encargan de realizar las operaciones y de manipular los datos de la aplicación. La capa de Interactor es la capa más interna de la aplicación y es la que se comunica con la capa de Presenter.

P - Presenter

La capa de Presenter es la encargada de contener la lógica de presentación de la aplicación. En esta capa se encuentran las clases que se encargan de preparar los datos para ser mostrados en la capa de View y de manejar las interacciones del usuario. La capa de Presenter es la capa que se encarga de comunicar la capa de View con la capa de Interactor.

E - Entity

La capa de Entity es la encargada de contener los datos de la aplicación. En esta capa se encuentran las clases que se encargan de representar los datos de la aplicación y de manipular los datos de la aplicación. La capa de Entity es la capa más interna de la aplicación y es la que se comunica con la capa de Interactor.

R - Router

La capa de Router es la encargada de contener la lógica de navegación de la aplicación. En esta capa se encuentran las clases que se encargan de navegar entre las diferentes pantallas de la aplicación y de comunicar las diferentes capas de la aplicación. La capa de Router es la capa más externa de la aplicación y es la que se comunica con la capa de Presenter.

Caso de uso

Supongamos el caso donde necesitamos hacer una aplicación web con muchos módulos y funcionalidades, como podría ser un sistema de gestión de inventario para una tienda en línea. En este caso, se podría aplicar el patrón VIPER debido a ciertos problemas técnicos y de diseño que se pueden presentar en el desarrollo de la aplicación, visualicemos los problemas técnicos en este caso de uso como lo serían:

  • Problemas de escalabilidad: La aplicación necesita ser escalable para poder soportar un gran número
  • Problemas de mantenimiento: La aplicación necesita ser fácil de mantener y de probar
  • Problemas de modularidad: La aplicación necesita ser modular para poder ser reutilizable en diferentes partes de la aplicación
  • Problemas de trabajo en equipo: La aplicación necesita ser fácil de trabajar en equipo y de colaborar con otros desarrolladores
  • Problemas de complejidad: La aplicación necesita ser fácil de entender y de depurar
  • Problemas de rendimiento: La aplicación necesita ser eficiente y rápida
  • Problemas de seguridad: La aplicación necesita ser segura y protegida contra ataques
  • Problemas de usabilidad: La aplicación necesita ser fácil de usar y de entender

Aplicacion que requieren multiples capas de seguridad, y que cada capa, módulo y funcion sea testeable es donde VIPER brillara por su arquitectura limpia y modular. Debido a que VIPER es un patrón de arquitectura de software que se utiliza en el diseño de aplicaciones móviles para separar las responsabilidades y mejorar la modularidad y la escalabilidad del código.

Ejemplo de estructura de carpetas

.
├── app
│ ├── modules
│ │ ├── module1
│ │ │ ├── view
│ │ │ ├── interactor
│ │ │ ├── presenter
│ │ │ ├── entity
│ │ │ ├── router
│ │ ├── module2
│ │ │ ├── view
│ │ │ ├── interactor
│ │ │ ├── presenter
│ │ │ ├── entity
│ │ │ ├── router
│ ├── shared
│ │ ├── view
│ │ ├── interactor
│ │ ├── presenter
│ │ ├── entity
│ │ ├── router

En este ejemplo, se puede ver la estructura de carpetas de una aplicación que utiliza el patrón VIPER. La carpeta app contiene los módulos de la aplicación, cada módulo tiene una carpeta con las capas de View, Interactor, Presenter, Entity y Router. La carpeta shared contiene las capas compartidas de la aplicación, que pueden ser utilizadas por varios módulos.

Tener en cuenta que la estructura de carpetas puede variar dependiendo de la aplicación y de las necesidades del proyecto, pero en general se recomienda seguir una estructura similar a la mostrada en este ejemplo para mantener el código organizado y fácil de mantener. No siga la estructura de carpetas a rajatabla, sino que adapte la estructura a las necesidades de su proyecto, esta solo es una idea de como podría ser la estructura de carpetas de una aplicación que utiliza el patrón VIPER, pero visualice mas a VIPER como una guía que le ayudará a tomar decisiones más acertadas en el diseño de software con los componentes de View, Interactor, Presenter, Entity y Router.

Aplicacion teorica

Supongamos que deseamos diseñar una aplicación de tipo red social donde los usuarios puedan publicar mensajes, compartir fotos, comentar publicaciones, dar me gusta a publicaciones, seguir a otros usuarios, enviar mensajes privados, crear grupos, crear eventos, crear encuestas.

Antes de arrancar al código, intente visualizar que conjuntos se arman en la solicitud de requerimientos, analice previamente lo solicitado, y analicemos juntos.

Para realizar una abstracción de la aplicación, podemos dividir la aplicación en diferentes módulos, como por ejemplo:

  • Módulo de Usuarios: Este módulo se encargaría de gestionar los usuarios de la aplicación, como el registro de nuevos usuarios, el inicio de sesión, la edición de perfil, la búsqueda de usuarios, la lista de seguidores, la lista de seguidos, la lista de amigos, etc.
  • Módulo de Publicaciones: Este módulo se encargaría de gestionar las publicaciones de los usuarios, como la creación de publicaciones, la edición de publicaciones, la eliminación de publicaciones, la lista de publicaciones, la lista de publicaciones de un usuario, la lista de publicaciones de un grupo, la lista de publicaciones de un evento, etc.
  • Módulo de Mensajes: Este módulo se encargaría de gestionar los mensajes de los usuarios, como el envío de mensajes, la recepción de mensajes, la lista de mensajes, la lista de mensajes de un usuario, la lista de mensajes de un grupo, la lista de mensajes de un evento, etc.
  • Módulo de Grupos: Este módulo se encargaría de gestionar los grupos de la aplicación, como la creación de grupos, la edición de grupos, la eliminación de grupos, la lista de grupos, la lista de miembros de un grupo, la lista de publicaciones de un grupo, etc.
  • Módulo de Eventos: Este módulo se encargaría de gestionar los eventos de la aplicación, como la creación de eventos, la edición de eventos, la eliminación de eventos, la lista de eventos, la lista de asistentes a un evento, la lista de publicaciones de un evento, etc.
  • Módulo de Encuestas: Este módulo se encargaría de gestionar las encuestas de la aplicación, como la creación de encuestas, la edición de encuestas, la eliminación de encuestas, la lista de encuestas, la lista de respuestas a una encuesta, etc.
  • Módulo de Notificaciones: Este módulo se encargaría de gestionar las notificaciones de la aplicación, como el envío de notificaciones, la recepción de notificaciones, la lista de notificaciones, la lista de notificaciones de un usuario, etc.

Cada módulo se puede implementar utilizando el patrón VIPER, donde cada módulo tendría las capas de View, Interactor, Presenter, Entity y Router. De esta forma, se puede separar las responsabilidades de cada módulo y mejorar la modularidad y la escalabilidad del código.

Aplicacion practica

Sigamos la aplicación teorica anteriormente vista, donde ya establecimos los módulos de la aplicación, ahora vamos a implementar el módulo de Usuarios utilizando el patrón VIPER.

En el ejemplo vamos a utilizar React + TypeScript para implementar el módulo de Usuarios utilizando el patrón VIPER. Vamos a crear las capas de View, Interactor, Presenter, Entity y Router para el módulo de Usuarios.

View

UserView.tsx

import React from "react";
interface UserViewProps {
users: User[];
onUserClick: (user: User) => void;
}
const UserView: React.FC<UserViewProps> = ({ users, onUserClick }) => {
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id} onClick={() => onUserClick(user)}>
{user.name}
</li>
))}
</ul>
</div>
);
};
export default UserView;

Interactor

UserInteractor.ts

import { User } from "./UserEntity";
class UserInteractor {
getUsers(): User[] {
// Get users from API
return [];
}
}
export default UserInteractor;

Presenter

UserPresenter.ts

import UserInteractor from "./UserInteractor";
class UserPresenter {
private interactor: UserInteractor;
constructor(interactor: UserInteractor) {
this.interactor = interactor;
}
getUsers(): User[] {
return this.interactor.getUsers();
}
}
export default UserPresenter;

Entity

UserEntity.ts

export interface User {
id: number;
name: string;
email: string;
}

Router

UserRouter.ts

import UserPresenter from "./UserPresenter";
class UserRouter {
private presenter: UserPresenter;
constructor(presenter: UserPresenter) {
this.presenter = presenter;
}
getUsers(): User[] {
return this.presenter.getUsers();
}
}
export default UserRouter;

En este ejemplo, se puede ver la implementación del módulo de Usuarios utilizando el patrón VIPER. La capa de View se encarga de renderizar la interfaz de usuario y de recibir las interacciones del usuario. La capa de Interactor se encarga de contener la lógica de negocio y de manipular los datos. La capa de Presenter se encarga de preparar los datos para ser mostrados en la capa de View y de manejar las interacciones del usuario. La capa de Entity se encarga de contener los datos de la aplicación. La capa de Router se encarga de contener la lógica de navegación de la aplicación.

Cuando aplicar VIPER

Por favor, no aplique de forma bruta y sin sentido VIPER, para aplicarlo correctamente al proyecto, requiere pasar previamente por un análisis de requerimientos, y visualizar que capas se pueden separar y que capas se pueden unir, reutilizar, escalar, mantener, probar, depurar, auditar, mejorar, colaborar, entender.

Antes de emocionarse y aplicar VIPER a su proyecto, analice previamente si puede separar las responsabilidades de las diferentes capas de la aplicación y de utilizar interfaces para comunicar entre ellas, esto permitirá que el código sea más modular, reutilizable y fácil de mantener, además de que facilitará la escritura de pruebas unitarias y la escalabilidad del código.

Debido a que una de las mayores desventajas de VIPER es que si no se define previamente un módulo puede ser una tarea tediosa estar reimplementando el patrón VIPER en cada módulo y estar separando algo que desde un inicio debio ser separado, para ello use el sentido común y aplique VIPER en los módulos que realmente lo necesiten.

Cuando no aplicar VIPER

No aplique VIPER a su proyecto si no necesita separar las responsabilidades de las diferentes capas de la aplicación y de utilizar interfaces para comunicar entre ellas, esto solo complicará el diseño de la aplicación y hará que el código sea más difícil de mantener y de entender.

No aplique VIPER a su proyecto si no necesita que el código sea más modular, reutilizable y fácil de mantener, ya que esto solo agregará complejidad innecesaria al diseño de la aplicación y hará que el código sea más difícil de entender y de depurar.

No aplique VIPER a su proyecto si no necesita facilitar la escritura de pruebas unitarias y la escalabilidad del código, ya que esto solo agregará complejidad innecesaria al diseño de la aplicación y hará que el código sea más difícil de probar y de auditar.

No aplique VIPER a su proyecto si no necesita que diferentes equipos de desarrollo trabajen en paralelo en diferentes partes de la aplicación, ya que esto solo agregará complejidad innecesaria al diseño de la aplicación y hará que el código sea más difícil de colaborar y de entender.

Consejos

Como programadores es facil e incluso tentativo empezar a crear carpetas, archivo y codigo, pero con VIPER es importante primero visualizar y analizar los requerimientos, y luego empezar a crear las carpetas, archivos y codigo. No se apresure, tómese su tiempo para analizar los requerimientos y para diseñar la arquitectura de la aplicación, esto le ayudará a tomar decisiones más acertadas y a evitar problemas en el futuro. Sea analítico, visualice, piense, analice, diseñe, cree, pruebe, depure, audite, mejore, colabore, entienda.

Arrancar con VIPER puede ser un poco complicado al principio, e incluso tomar mucho tiempo, (el cual es una ventaja y desventaja), pero gracias al tiempo que invierta en el diseño de la arquitectura de la aplicación, le permitirá tener un código más modular, reutilizable y fácil de mantener, ademas de que facilitará la escritura de pruebas unitarias y la escalabilidad del código. Asi que no caiga en el error de que por prisas o por querer terminar rápido, no analice y no diseñe correctamente la arquitectura de la aplicación, es importante arrancar correctamente ya que son los pilares de la aplicación, todos estarán basados en la arquitectura de la aplicación, asi que sea cuidadoso y analítico.

Piense en equipo, piense en la comunidad, piense en el futuro.