Clean architecture en Flutter

Alfredo Bautista Santos
Google for Developers Europe
7 min readDec 28, 2020

--

¡Buenas a todos! En este artículo veremos una pequeña introducción a Clean Architecture, como estructurar un proyecto de Flutter para que dimensione sin problemas y manteniendo un mínimo acople entre componentes.

¿Que es clean architecture?

Clean Architecture es un conjunto de principios y patrones de diseño que deben facilitar el proceso de construcción del software, así como su mantenimiento y escalado.

Clean Architecture es un nombre popularizado por Robert Cecil Martin, conocido como “Uncle Bob” con la base de estructura el código en capas o “layers” que solo se comunican con sus capas contiguas. En estos artículos puedes encontrar mas información sobre Clean Architecture, Onion Architecture, Hexagonal Architecture. Todas ellas tienen diferentes enfoques, pero comparten la idea de que cada nivel tiene su responsabilidad y se comunica únicamente con sus niveles inmediatamente contiguos.

Esta series de recomendaciones y patrones son agnósticas de la plataforma o lenguaje de programación que usemos para desarrollar nuestras aplicaciones. Ahora la veremos enfocadas en Dart y Flutter, usando algunas ventajas que nos proporciona su lenguaje y el framework.

Las capas

Empecemos comprendiendo que significan las capas y los diferentes niveles de las mismas que tendremos en nuestra aplicación.

Una capa es un conjunto de clases, paquetes o ficheros que tienen unas responsabilidades relacionadas dentro del sistema. Estas capas están organizadas de forma jerárquica unas encima de otras y las dependencias siempre van hacia abajo. Es decir, que una capa dependerá solamente de las capas inferiores, pero nunca de las superiores.

Por ejemplo, la capa de UI, representado en el gráfico anterior en la capa superior, dependerá de la capa de Dominio que contiene nuestra lógica de negocio pero nuestro Dominio será agnóstico de la UI a la que este sirviendo y no variará dependiendo de ella.

  • UI: Capa encargada de la representación de los datos en un dispositivo o plataforma. En nuestro caso, será todo el código de Flutter correspondiente a nuestros Widgets, pages, navegación…
  • Data: Capa encargada de comunicarse con las dependencias externas que necesita nuestra app para obtener los datos. Por ejemplo, la implementación concreta de los repositories con llamadas HTTP, Firebase…
  • Device: Capa encargada de acceso a las funcionalidades nativas del dispositivo en el que correrá nuestra app. Por ejemplo, acceso a la biometría, uso de GPS…
  • Domain: Capa que engloba toda la lógica de negocio de nuestra app, donde se encuentra el código que debe ser agnóstico de cualquier otra parte de nuestro software. En este caso, es puro código Dart.

Interfaces

Para llevar a cabo esta comunicación entre las capas usaremos las interfaces.

¿Qué es una interfaz?

Una interfaz es un concepto que nos permite abstraer casos de uso de nuestra lógica de negocio de su implementación en nuestro sistema. Es decir, objetos que tienen una mínima definición de sus métodos y hacen de “molde” o “guía” para que las clases que sí concreten estos métodos sigan las definiciones de las reglas de negocio.

Usando este concepto, podemos aplicarlo a nuestra aplicación de Flutter usando clases abstractas que expondrán una serie de métodos que deben ser implementados por las dependencias de una capa inferior y este será el puente que una las diferentes capas.

Veámoslo mejor con un ejemplo.

Tenemos una interfaz que incorpora los métodos doLogin y doLogout que son necesarios en nuestra aplicación, pero no su implementación dependiendo de la plataforma.

Ahora imaginemos que hemos elegido Firebase como nuestro servidor de autenticación, en ese caso lo implementaremos.

Ahora supongamos que queremos cambiar nuestro backend de Firebase a uno auto-gestionado, por lo que usaremos llamadas HTTP.

Esta estrategia nos ofrece muchas ventajas:

  • Desacople de dependencias externas de nuestras reglas de negocio
  • Esto nos permite tener varias implementaciones y poder cambiar fácilmente entre ellas, sin modificar la capa de Dominio.
  • Podemos crear una implementación mockeada para permitir el testing.

¡Manos a la obra!

Vamos a desglosar como personalmente me gusta estructurar mis proyectos en Flutter, siguiendo los anteriores conceptos y añadiendo los conceptos propios del framework.

Capa de UI

Capa que contiene todos los componentes encargados de representar la información de la app a los usuarios. Principalmente, dividimos nuestros componentes en 3 grandes grupos:

  • Pages: Contiene los ficheros que representan nuestras pantallas y dentro de estas carpetas también podemos tener una subcarpeta llamada Widgets que contiene los elementos que componen nuestra pantalla
  • Widgets: Carpeta que contiene los componentes que se usan a través de toda la aplicación. Esta forma de trabajar está aconsejada por el equipo de Flutter, crear Widgets independiente y reusables.
  • Utils: Son los ficheros que no corresponden a las otras dos categorías pero forman parte de la capa de UI y de nuestro framework. Algunos de estos ejemplos son un Router que nos defina y contenga las rutas, un generador de pantallas con su inyección de dependencias de cada Page…

Capa de Dominio

En esta capa se encontrarán todos los componentes que definen nuestra lógica de negocio.

  • BLoCs: En esta capa se encuentran los Business Logic Components donde se encontrará toda la lógica de negocio de cada componente y su definición de evento/estado. Si no conoces este patrón, te recomiendo este vídeo de la DartConference de 2018
  • Entities: Son las clases que representan nuestras entidades de la capa de negocio. Contienen sus métodos de interacción además de su serialización y deserialización.
  • Repositories: Componentes que se dedican a obtener información de servicios externos y crear los objetos necesarios para el funcionamiento de la app. En esta capa solo se encuentran sus interfaces, como hemos visto anteriormente.
  • Services: Componentes que interaccionan con servicios externos de nuestra app. Solo se encuentran sus interfaces, como en el caso de los repositories.

Con estos cuatro bloques podríamos tener suficiente para comenzar la distribución de los componentes en nuestra app y que permita dimensionar bastante bien, si queremos seguir diferenciando componentes para un mayor desacople podremos crear la carpeta Errors que contendrán nuestras entidades que representen errores externos o internos en nuestra app y otra carpeta Interactors que será el encargado de orquestar varios servicios o repositorios encargados de llevar a cabo una función.

Capa de Datos

En esta capa encontraremos las implementaciones de las interfaces de nuestra capa de Domain.

  • Repositories: Encontraremos las diferentes implementaciones de cada interfaz de los Repositories, personalmente me gusta agruparlo por interfaz, ya que podemos tener varias implementaciones de una misma clase.
  • Services: Muy parecido a la anterior pero con las interfaces de nuestros Services.

En esta capa también tendremos las implementaciones Mockeadas para nuestros test y también algunos Fakes por si necesitamos datos específicos en nuestra app para hacer pruebas.

Capa Device

En esta capa encontraremos las dependencias nativas de los dispositivos donde correrá nuestra app. El contenido de esta carpeta es bastante flexible ya que dependerá de las necesidades de nuestra lógica de negocio. Algunos ejemplos son el acceso a GPS, biometrics, cámara…

Bonus Layer: Inyección de dependencias

Digamos que esta capa no es estrictamente necesaria y que con las capas anteriores ya tienes una buena estructuración de tu proyecto, pero si quieres seguir el principio de DI, que puedes leer en este artículo implementado en Flutter, debes implementar esta capa.

En esta capa encontraremos los diferentes componentes que serán los responsables de inyectar las dependencias entre ellas y en la app, veámoslo con un ejemplo inyectando el auth_repository en nuestro auth_bloc.

En este ejemplo, donde cada clase corresponde a un fichero, vemos como esa inyección de dependencias nos permite tener esta capa como solo registrador de las dependencias de cada servicio, repositorio, bloc…

Con esta capa, únicamente deberemos llamar a nuestro injector con el tipo que queremos y ya nos dará la instancia construida con los dependencias creadas, sin que necesitemos saber cómo o dónde se construyó.

Además de un cambio de dependencias muy ágil, si ahora quisiéramos cambiar la dependencia de nuestro bloc, únicamente debemos modificar el register de nuestro AbstractLoginRepository y ninguna parte de nuestra app debe ser modificada

Resumen

Siguiendo estas pequeñas pautas y entendiendo el porqué de esta división en capas, nos permitirá una buena estructuración de nuestras aplicaciones de Flutter. También debemos tener en cuenta que estas pautas son flexibles y nos deben ayudar, si no nos encaja no tiene ninguna ventaja usar este tipo de arquitectura.

Podéis también complementar la información de este artículo con el vídeo que he creado hablando de esta estructuración en un proyecto de Flutter.

Por ultimo dejo os mi twitter dónde me encontraréis hablando sobre tecnología en general pero sobre todo Flutter 💙

¡Un saludo y hasta la próxima!

--

--

Alfredo Bautista Santos
Google for Developers Europe

Sysadmin and web developer. Co-organizer of GDGMarbella & FlutterConf in Marbella, Spain. Flutter enthusiastic.