Saltar al contenido principal

Guía de arquitectura de la aplicación

La forma recomendada de estructurar la arquitectura de una app Flutter.

Las siguientes páginas demuestran cómo construir una app utilizando las mejores prácticas. Las recomendaciones de esta guía se pueden aplicar a la mayoría de las apps, haciéndolas más fáciles de escalar, probar y mantener. Sin embargo, son pautas, no reglas inquebrantables, y debes adaptarlas a tus requisitos únicos.

Esta sección proporciona una perspectiva general de alto nivel de cómo se puede estructurar la arquitectura de las aplicaciones Flutter. Explica las capas de una aplicación, junto con las clases que existen dentro de cada capa. La sección posterior a esta proporciona ejemplos de código concretos y recorre una aplicación Flutter que ha implementado estas recomendaciones.

Perspectiva general de la estructura del proyecto

#

La separación de responsabilidades (Separation-of-concerns) es el principio más importante a seguir cuando diseñas tu app en Flutter. Tu aplicación Flutter debe dividirse en dos grandes capas: la capa de UI y la capa de datos.

Cada capa se divide a su vez en diferentes componentes, cada uno de los cuales tiene responsabilidades distintas, una interfaz bien definida, límites y dependencias. Esta guía recomienda dividir tu aplicación en los siguientes componentes:

  • Vistas
  • View models
  • Repositorios
  • Servicios

MVVM

#

Si te has encontrado con el patrón de arquitectura Model-View-ViewModel (MVVM), esto te resultará familiar. MVVM es un patrón arquitectónico que separa una funcionalidad de una aplicación en tres partes: el Modelo, el ViewModel y la Vista. Las vistas y los view models componen la capa de UI de una aplicación. Los repositorios y servicios representan los datos de una aplicación, o la capa del modelo de MVVM. Cada uno de estos componentes se define en la siguiente sección.

Patrón de arquitectura MVVM

Cada funcionalidad en una aplicación contendrá una vista para describir la UI y un view model para manejar la lógica, uno o más repositorios como fuentes de verdad para los datos de tu aplicación, y cero o más servicios que interactúen con APIs externas, como servidores cliente y plugins de plataforma.

Una única funcionalidad de una aplicación podría requerir todos los siguientes objetos:

Un ejemplo de los objetos Dart que podrían existir en una funcionalidad utilizando la arquitectura descrita en la página.

Cada uno de estos objetos y las flechas que los conectan se explicarán detalladamente al final de esta página. A lo largo de esta guía, se utilizará la siguiente versión simplificada de ese diagrama como ancla.

Un diagrama simplificado de la arquitectura descrita en esta página.

Capa de UI

#

La capa de UI de una aplicación es responsable de interactuar con el usuario. Muestra los datos de la aplicación al usuario y recibe la entrada del usuario, como eventos de toque (tap) y entradas de formularios.

La UI reacciona a los cambios de datos o a la entrada del usuario. Cuando la UI recibe nuevos datos de un repositorio, debe volver a renderizarse para mostrar esos nuevos datos. Cuando el usuario interactúa con la UI, debe cambiar para reflejar esa interacción.

La capa de UI está compuesta por dos componentes arquitectónicos, basados en el patrón de diseño MVVM:

  • Vistas (Views) describen cómo presentar los datos de la aplicación al usuario. Específicamente, se refieren a las composiciones de Widgets que conforman una funcionalidad. Por ejemplo, una vista es a menudo (pero no siempre) una pantalla que tiene un Widget Scaffold, junto con todos los Widgets debajo de él en el árbol de Widgets. Las vistas también son responsables de pasar eventos al view model en respuesta a las interacciones del usuario.
  • Los View models contienen la lógica que convierte los datos de la app en UI State, porque los datos de los repositorios a menudo tienen un formato diferente al de los datos que se deben mostrar. Por ejemplo, es posible que necesites combinar datos de múltiples repositorios, o que desees filtrar una lista de registros de datos.

Las vistas y los view models deberían tener una relación de uno a uno.

Un diagrama simplificado de la arquitectura descrita en esta página con los objetos view y view model resaltados.

En los términos más sencillos, un view model gestiona el UI State y la vista muestra ese State. Al utilizar vistas y view models, tu capa de UI puede mantener el State durante los cambios de configuración (como las rotaciones de pantalla), y puedes probar la lógica de tu UI de forma independiente de los Widgets de Flutter.

Una funcionalidad de una aplicación está centrada en el usuario, y por lo tanto definida por la capa de UI. Cada instancia de un par view y view model define una funcionalidad en tu app. Esto suele ser una pantalla en tu app, pero no tiene por qué serlo. Por ejemplo, considera iniciar y cerrar sesión. Iniciar sesión generalmente se realiza en una pantalla específica cuyo único propósito es proporcionar al usuario una forma de iniciar sesión. En el código de la aplicación, la pantalla de inicio de sesión estaría compuesta por una clase LoginViewModel y una clase LoginView.

Por otro lado, cerrar sesión en una app generalmente no se realiza en una pantalla dedicada. La capacidad de cerrar sesión se presenta generalmente al usuario como un botón en un menú, una pantalla de cuenta de usuario o en cualquier otro lugar. A menudo se presenta en múltiples ubicaciones. En tales escenarios, podrías tener un LogoutViewModel y una LogoutView que solo contiene un único botón que se puede colocar dentro de otros Widgets.

Vistas

#

En Flutter, las vistas son las clases Widget de tu aplicación. Las vistas son el método principal para renderizar la UI, y no deben contener ninguna lógica de negocio. Se les debe pasar todos los datos que necesitan para renderizar desde el view model.

Un diagrama simplificado de la arquitectura descrita en esta página con el objeto view resaltado.

La única lógica que debería contener una vista es:

  • Sentencias if simples para mostrar y ocultar Widgets basados en un indicador (flag) o campo anulable (nullable) en el view model
  • Lógica de animación
  • Lógica de diseño (layout) basada en la información del dispositivo, como el tamaño de la pantalla o la orientación.
  • Lógica de enrutamiento simple

Toda la lógica relacionada con los datos debería manejarse en el view model.

View models

#

Un view model expone los datos de la aplicación necesarios para renderizar una vista. En el diseño de arquitectura descrito en esta página, la mayor parte de la lógica en tu aplicación Flutter reside en los view models.

Un diagrama simplificado de la arquitectura descrita en esta página con el objeto view model resaltado.

Las responsabilidades principales de un view model incluyen:

  • Obtener datos de la aplicación desde los repositorios y transformarlos en un formato adecuado para la presentación en la vista. Por ejemplo, podría filtrar, ordenar o agregar datos.
  • Mantener el State actual necesario en la vista, de modo que la vista pueda reconstruirse sin perder datos. Por ejemplo, podría contener indicadores booleanos para renderizar condicionalmente Widgets en la vista, o un campo que haga un seguimiento de qué sección de un carrusel está activa en la pantalla.
  • Expone callbacks (llamados commands) a la vista que se pueden conectar a un manejador de eventos, como presionar un botón o enviar un formulario.

Los Commands (comandos) reciben su nombre del patrón command, y son funciones de Dart que permiten a las vistas ejecutar lógica compleja sin conocimiento de su implementación. Los comandos se escriben como miembros de la clase view model para ser llamados por los manejadores de gestos en la clase de la vista.

Puedes encontrar ejemplos de vistas, view models y comandos en la parte de capa de UI del estudio de caso de arquitectura de la app.

Para una introducción sencilla a MVVM en Flutter, consulta los fundamentos de State Management.

Capa de datos

#

La capa de datos de una app maneja los datos y la lógica de negocio. Dos piezas de arquitectura componen la capa de datos: servicios y repositorios. Estas piezas deben tener entradas y salidas bien definidas para simplificar su reutilización y testabilidad.

Un diagrama simplificado de la arquitectura descrita en esta página con la capa de datos resaltada.

Utilizando el lenguaje MVVM, los servicios y repositorios componen tu capa de modelo.

Repositorios

#

Las clases Repository son la fuente de verdad para tus datos de modelo. Son responsables de sondear (poll) datos de los servicios, y transformar esos datos brutos en modelos de dominio (domain models). Los modelos de dominio representan los datos que la aplicación necesita, formateados de manera que tus clases de view model puedan consumirlos. Debería haber una clase repository para cada tipo diferente de datos que se maneje en tu app.

Los repositorios manejan la lógica de negocio asociada con los servicios, como:

  • Caching
  • Manejo de errores
  • Lógica de reintento
  • Actualizar datos
  • Realizar sondeos (polling) a los servicios para obtener nuevos datos
  • Actualizar datos basados en las acciones del usuario

Un diagrama simplificado de la arquitectura descrita en esta página con el objeto Repository resaltado.

Los repositorios entregan los datos de la aplicación como modelos de dominio. Por ejemplo, una app de redes sociales podría tener una clase UserProfileRepository que expone un Stream<UserProfile?>, el cual emite un nuevo valor cada vez que el usuario inicia o cierra sesión.

Los modelos entregados por los repositorios son consumidos por los view models. Los repositorios y los view models tienen una relación de muchos a muchos. Un view model puede usar muchos repositorios para obtener los datos que necesita, y un repositorio puede ser utilizado por muchos view models.

Los repositorios nunca deben tener conocimiento el uno del otro. Si tu aplicación tiene lógica de negocio que necesita datos de dos repositorios, debes combinar los datos en el view model o en la capa de dominio, especialmente si tu relación entre repositorio y view model es compleja.

Manejar el State de sesión a nivel de toda la app

#

Debido a que los repositorios son la única fuente de verdad para los datos de la aplicación, también son el lugar ideal para gestionar el State del ciclo de vida a nivel de toda la app; un State que necesita compartirse entre múltiples view models pero que no debe persistir más allá de la sesión actual de la aplicación.

Ejemplos de State del ciclo de vida a nivel de toda la app incluyen una sesión de usuario activa, cachés de datos en memoria o configuraciones transitorias de la aplicación. Debido a que los view models y los repositorios tienen una relación de muchos a muchos, múltiples view models pueden depender de la misma instancia de repositorio (típicamente gestionada a través de un localizador de servicios o un contenedor de inyección de dependencias). Esto permite que distintas funcionalidades observen y modifiquen de forma reactiva el mismo State compartido a través de streams y métodos expuestos por el repositorio, sin violar el límite limpio de uno a uno entre una vista y su view model.

Servicios

#

Los servicios se encuentran en la capa más baja de tu aplicación. Envuelven endpoints de API y exponen objetos de respuesta asíncronos, tales como objetos Future y Stream. Solo se utilizan para aislar la carga de datos y no contienen ningún State. Tu app debería tener una clase de servicio por cada fuente de datos. Ejemplos de endpoints que los servicios podrían envolver incluyen:

  • La plataforma subyacente, como las APIs de iOS y Android
  • Endpoints REST
  • Archivos locales

Como regla general, los servicios son más útiles cuando los datos necesarios residen fuera del código Dart de tu aplicación; lo cual es cierto para cada uno de los ejemplos anteriores.

Los servicios y los repositorios tienen una relación de muchos a muchos. Un solo Repository puede usar varios servicios, y un servicio puede ser utilizado por múltiples repositorios.

Un diagrama simplificado de la arquitectura descrita en esta página con el objeto Service resaltado.

Opcional: Capa de dominio

#

A medida que tu app crece y agrega funcionalidades, es posible que necesites abstraer lógica que añada demasiada complejidad a tus view models. Estas clases a menudo se denominan interactores o casos de uso (use-cases).

Los casos de uso son responsables de hacer que las interacciones entre las capas de UI y de datos sean más simples y reutilizables. Toman datos de los repositorios y los adaptan para la capa de UI.

Patrón de diseño MVVM con un objeto de capa de dominio agregado

Los casos de uso se utilizan principalmente para encapsular lógica de negocio que de otro modo residiría en el view model y que cumple con una o más de las siguientes condiciones:

  1. Requiere combinar datos de múltiples repositorios
  2. Es sumamente compleja
  3. La lógica será reutilizada por diferentes view models

Esta capa es opcional porque no todas las aplicaciones o funcionalidades dentro de una aplicación tienen estos requisitos. Si sospechas que tu aplicación se beneficiaría de esta capa adicional, considera los pros y contras:

ProsContras
✅ Evita la duplicación de código en los view models ❌ Incrementa la complejidad de tu arquitectura, agregando más clases y una mayor carga cognitiva
✅ Mejora la testabilidad al separar la lógica de negocio compleja de la lógica de UI ❌ Las pruebas requieren mocks adicionales
✅ Mejora la legibilidad del código en los view models ❌ Agrega boilerplate (código repetitivo) adicional a tu código

Acceso a datos con casos de uso (use-cases)

#

Otra consideración al agregar una capa de dominio es si los view models seguirán teniendo acceso directo a los datos del repositorio, o si obligarás a los view models a pasar a través de los casos de uso para obtener sus datos. Dicho de otra manera, ¿agregarás casos de uso a medida que los necesites? ¿Quizás cuando notes lógica repetida en tus view models? ¿O crearás un caso de uso cada vez que un view model necesite datos, incluso si la lógica en el caso de uso es simple?

Si eliges hacer esto último, se intensifican los pros y contras descritos anteriormente. El código de tu aplicación será extremadamente modular y testable, pero también agregará una cantidad significativa de sobrecarga innecesaria.

Un buen enfoque consiste en agregar casos de uso únicamente cuando sea necesario. Si descubres que tus view models están accediendo a los datos a través de casos de uso la mayor parte del tiempo, siempre puedes refactorizar tu código para utilizar casos de uso exclusivamente. La app de ejemplo utilizada más adelante en esta guía tiene casos de uso para algunas funcionalidades, pero también tiene view models que interactúan directamente con los repositorios. Una funcionalidad compleja podría, en última instancia, verse así:

Un diagrama simplificado de la arquitectura descrita en esta página con un objeto de caso de uso.

Este método para agregar casos de uso está definido por las siguientes reglas:

  • Los casos de uso dependen de los repositorios
  • Los casos de uso y los repositorios tienen una relación de muchos a muchos
  • Los view models dependen de uno o más casos de uso y uno o más repositorios

Este método de utilizar casos de uso termina pareciéndose menos a una lasaña en capas y más a una cena emplatada con dos platos principales (capas de UI y de datos) y un acompañamiento (capa de dominio). Los casos de uso son solo clases de utilidad que tienen entradas y salidas bien definidas. Este enfoque es flexible y extensible, pero requiere una mayor diligencia para mantener el orden.

Comentarios

#

Dado que esta sección del sitio web está evolucionando, ¡agradecemos tus comentarios!