Jaime Gil de Sagredo Luna profile's photo

Jaime Gil de Sagredo Luna

Clean Architecture y manejo de estado en Flutter: un enfoque simple y efectivo

Crosspost de tappr.dev.

¿Qué es Clean Architecture y por qué es importante?

Clean Architecture es un término popularizado por Robert C. Martin que describe una buena arquitectura para la estructura de las distintas partes que componen el código fuente de una aplicación, y de las conexiones necesarias entre dichas partes.

¿Y qué entendemos por una “buena arquitectura”? Es aquella que posibilita a los desarrolladores de una aplicación comprender su funcionamiento con facilidad, y por tanto, les permite modificar y evolucionar la aplicación de manera rápida, sostenible en el tiempo, y sin introducir defectos durante el proceso.

Diagrama simplificado de una buena arquitectura.

Aparte de Clean Architecture hay otras aproximaciones muy similares que definen su propia implementación de lo que es una buena arquitectura de software (Hexagonal Architecture u Onion Architecture, por ejemplo). Pero la principal característica de todas ellas es la separación de responsabilidades entre lógica de negocio e interfaz de usuario, limitando que la primera no tenga ningún conocimiento de los detalles de la segunda.

A continuación vamos a aplicar estos principios para definir una buena arquitectura para nuestras aplicaciones en Flutter, sin que esto suponga un aumento accidental de la complejidad de nuestro software. Con ello obtendremos una sencilla base que nos permitirá construir y evolucionar a lo largo del tiempo nuestras aplicaciones.

¿Cómo desarrollar interfaces de usuario con Flutter?

Como acabamos de ver, una buena arquitectura debe separar dos responsabilidades principales: la lógica de negocio y la interfaz de usuario. En el caso del desarrollo de aplicaciones con Flutter, éste será nuestro framework de interfaces de usuario, por lo que tenemos que entender cómo funciona y cuáles son sus características.

Flutter es un framework declarativo para diseñar interfaces gráficas, como React, Swift UI o Jetpack Compose (contrapuesto a opciones imperativas como UIKit o Android SDK). Ésto significa que Flutter dibuja la interfaz de usuario reflejando el “estado” de la aplicación.

Fórmula matemática de UI = f(state). 'UI' es el diseño de la pantalla. 'f' es la función de construcción. 'state' es el estado de la aplicación.

Por ejemplo, cuando un usuario pulsa un botón que cambia el estado de la aplicación, la interfaz se redibuja automáticamente para reflejar este nuevo estado. Este enfoque declarativo para el diseño de interfaces tiene muchos beneficios, pero destaca especialmente el hecho de que sólo hay un camino en el código para definir cómo se muestra la interfaz.

Esta característica se alinea de maravilla con nuestro requisito de delimitar la comunicación entre la lógica de negocio y la interfaz de usuario. Aunque utilizar un framework declarativo no es un requisito para implementar una buena arquitectura, ya que podemos abstraer cualquier enfoque de creación de interfaces para satisfacer nuestras necesidades, ésto nos simplifica bastante la implementación de nuestra arquitectura.

¿Cómo manejar el estado en Flutter?

Podemos distinguir dos tipos de estados que necesitamos manejar en Flutter. Un primer estado “efímero”, o de interfaz de usuario, y un segundo estado “persistente”, o estado de negocio. Como puedes ver, cada tipo de estado pertenece inequívocamente a una de las responsabilidades definidas por una buena arquitectura.

Ejemplos de estado de interfaz de usuario pueden ser el número de página actual en un listado, el progreso de una animación, la pestaña seleccionada en la navegación, etc. Por otro lado, ejemplos de estado de negocio pueden ser las preferencias del usuario, el contenido de un carrito de compra, las reseñas de un producto, etc. También tenemos que tener en cuenta que, lo que para una aplicación puede ser estado de interfaz, para otra puede ser estado de negocio (por ejemplo, si queremos que la pestaña de navegación seleccionada se mantenga tras cerrar la aplicación, o incluso se sincronice entre todos los dispositivos del usuario).

Cualquier cambio en el estado de la aplicación, ya sea de interfaz o de negocio, puede provocar un redibujado de la interfaz de usuario si lo necesitamos.

Aquí vamos a ver un ejemplo de cambio de estado de interfaz muy sencillo. Se trata de un estado efímero que perdura en memoria únicamente hasta el cierre de la aplicación. Para ello vamos a usar la primitiva setState de Flutter:

Esta misma lógica la podemos implementar como estado de negocio en el caso de que sea un requisito funcional de nuestra aplicación, por ejemplo, recuperando y persistiendo el contador usando un API HTTP. Para actualizar la interfaz de usuario seguiremos usando la primitiva setState de Flutter:

En este caso también hemos añadido una sencilla pantalla de carga, ya que la comunicación con el API es asíncrona, y un mensaje de error en caso de que se produzca algún problema en el proceso.

¿Cómo implementar Clean Architecture en Flutter?

Hasta ahora hemos visto dos ejemplos muy sencillos de cómo manejar el estado de una aplicación en Flutter, y cómo desencadenar la actualización de la interfaz de usuario para reflejar los cambios.

En estos ejemplos, la lógica de negocio se reduce únicamente a dos llamadas a nuestro cliente API (que abstrae todos los detalles de comunicación, autenticación y parseo). Pero todo el flujo de negocio se produce en nuestro código de interfaz (en un widget de Flutter, en este caso). Para aplicaciones pequeñas esta separación podría parecer suficiente, pero podemos acabar mezclando código de negocio e interfaz rápidamente, haciendo nuestra implementación cada vez más frágil y difícil de evolucionar.

Es por eso que uno de los objetivos de esta arquitectura, y debería serlo de cualquier buena arquitectura, es que sea efectiva desde un primer momento (independientemente de si prevemos que la aplicación vaya a ser más o menos grande o exitosa). Tiene que ser fácil empezar a desarrollar cualquier aplicación sin ningún tipo de complejidad adicional a la esencial del negocio, y permitirnos evolucionarla de forma sostenida en el tiempo.

Diagrama detallado de una buena arquitectura.

Así que por último, vamos a definir dónde ejecutar la lógica de negocio, y cómo realizar la comunicación con la interfaz. Para ello vamos a utilizar un patrón muy extendido en el desarrollo de interfaces de usuario, y que recibe distintos nombres y ligeros cambios en su implementación dependiendo del framework o arquitectura en el que se enmarque. Nosotros lo vamos a llamar “interacción”, ya que encapsula una acción o interacción concreta y unitaria del usuario con la aplicación. Esta interacción es equivalente al “presenter” en Model-View-Presenter o Humble View , al “view model” en Model-View-ViewModel o al “use case” en Clean Architecture . Lo más importante a la hora de aplicar este patrón es respetar la separación de responsabilidades, y la dirección de la comunicación.

Vamos a modificar el último ejemplo extrayendo las interacciones con nuestra lógica de negocio:

Ahora el código de interfaz sólo es responsable de ejecutar la interacción que requiere el usuario en cada momento y actualizar la interfaz con el resultado. Es la interacción la que lleva a cabo todo el flujo de negocio (haciendo uso del cliente API, procesando la respuesta y los posibles errores, y notificando a la interfaz del resultado).

De esta forma hemos conseguido separar claramente las distintas responsabilidades del código, y además ganamos claridad sobre las interacciones a las que tiene acceso un usuario de la aplicación, facilitando el entendimiento del funcionamiento de la aplicación por parte de los desarrolladores.

Además de los patrones que hemos visto aquí, también aplicaremos otros patrones de diseño de software comunes en el resto de arquitecturas mencionadas anteriormente, como el patrón repositorio para el acceso a datos.

Aquí puedes ver un ejemplo completo de una sencilla aplicación implementada siguiendo esta arquitectura.

¿Por qué no usar una librería externa de manejo de estado en Flutter?

Con esta arquitectura deja de tener sentido el uso de librerías externas para manejo de estado como Riverpod o Bloc, entre otras, ya que el estado de la aplicación se simplifica al máximo y las primitivas existentes en el propio framework son más que suficientes.

De por sí estas librerías suelen ser contraproducentes para conseguir una buena arquitectura (Riverpod flaws, Riverpod or Bloc), ya que introducen conceptos propios y complejos que requieren una pronunciada curva de aprendizaje, como “Cubit”, “Provider”, “Reference”, etc. Incluso suelen requerir más código repetitivo que sin ellas para definir y manejar el estado de la aplicación, por lo que además no son efectivas para aplicaciones más pequeñas. Por último, pueden no soportar escenarios más complejos, y suponen un gran vector de errores al poder introducir bugs, actualizaciones sin compatibilidad, fallos de seguridad, etc.

Es cierto que aquí hemos visto un ejemplo muy sencillo de actualización de estado, pero es muy fácil aplicar esta arquitectura para usar streams de datos, u otras estructuras de datos, y actualizar la interfaz en tiempo real sin necesidad de hacer ninguna consulta, por ejemplo.

Conclusión

Habrás notado que hemos dejado de lado la típica jerga que solemos escuchar cuando hablamos de arquitectura de software, y nos hemos centrado en los conceptos fundamentales para entender qué es una buena arquitectura. Conceptos como capas, entidades, gateways, puertos y adaptadores, etc., complejizan muchas veces el entendimiento y aplicación de una arquitectura limpia, pero sin duda también es necesario estudiarlos y aplicarlos cuando corresponda.

En el repositorio de ejemplo puedes ver algunos de estos conceptos aplicados, lo cual facilita su comprensión. En él también puedes encontrar una estrategia de testing moderna que se alinéa con los fundamentos de una buena arquitectura.

Si te ha parecido interesante y te gustaría profundizar en todo lo relativo a buena arquitectura de software, testing y desarrollo de alta calidad, puedes contactarme y te ayudaré a aplicarlo en tu empresa.

Discussion