Seguimos estos tips en nuestro equipo Mobile Flutter y nuestra productividad se disparó

Carlos Daniel
Building La Haus
Published in
10 min readNov 2, 2022
Photo by Jon Tyson on Unsplash

No es lo mismo implementar un producto cuando no tenemos ningún tipo de guía y elementos de desarrollo definidos, a cuando verdaderamente tenemos algo que nos permita, además de desarrollar a buen ritmo nuestra app, implementarla de manera escalable y mantenible. Para poder lograrlo, la literatura y backgrounds de cada uno de los miembros del equipo, influyen a sobremanera. Cada persona viene de experiencias diferentes, exigentes, exitosas, o con fracasos y malas prácticas de desarrollo. Pero lo más interesante es que de todo ello, cuando confluyen las buenas ideas y los buenos profesionales, además de un muy buen conocimiento técnico, lo que se obtiene son los mencionados productos que van evolucionando, con cada iteración y el dolor de ir escalando desde el punto de vista desarrollo es cada vez menor.

Estos son algunas de nuestras buenas prácticas en nuestro equipo de desarrollo Mobile Flutter en La Haus que queremos compartir.

1. Diseñar de una Arquitectura clara de trabajo

Ya lo habíamos mencionado en un escrito anterior — sección “Una arquitectura base y clara”, pero me gustaría insistir en que este punto ha sido vital para que nuestro producto siga iterando y creciendo sin tener aún que remover esas fundaciones que lo sostienen.

Usamos Provider como nuestro manejador de estados y lo hacemos de la mano de una implementación propia de clases ViewModel (con ChangeNotifier) y una clase base Status, que mapea cada estado en cualquier momento de nuestra UI.

De ahí en adelante, patrón repository + remote services y en algunas eventualidades con Interactors (también llamados “use cases”) completan el resto de nuestra arquitectura. Esto nos ha permitido poder tener facilidad y flexibilidad de tener un cubrimiento de unit tests para todos los ViewModel, Interactors y repositories (lo cual ha sido clave para la estabilidad y cohesión).

Y rescatando un comentario que compartíamos en el escrito en mención: “Lo más importante es que nuestra arquitectura no es un tema fixed, al contrario, para nosotros está siempre en constante evolución”

2. Separar lógica de negocio y lógica de UI

Se relaciona con el punto anterior, aunque nos gustaría hacer un zoom del qué y el cómo.

Para poder tener una separación de responsabilidades seguimos:

  • Para cada miembro del equipo esto es un implícito. Siempre separamos la capa de UI de la capa de negocio. Tenemos nuestras clases Widgets con solamente elementos de UI, que instancian sus respectivos ViewModelcapa con la lógica de negocio — (nuestra regla usualmente es que cada Widget puede tener 0 o 1 ViewModel asociado).
  • En los Widgets solamente tenemos código Flutter (solo elementos del framework) y en los ViewModel solamente código Dart (nada de Flutter). Esto nos ha permitido velocidad en la implementación, y rapidez ante solución de errores. Ha facilitado la implementación de los unit tests y nos ha dado la posibilidad de una forma muy propia de nosotros de gestionar el estado de la UI sin tener que tener lógica dentro de cada widget.
  • El Widget solo “pinta” lo que le enviamos, no tiene un bloque de código con lógica de negocio que permita habilitar o deshabilitar un botón. Todo esto se hace en el ViewModel.
  • El ViewModel usualmente trabaja en conjunto con una clase Status definida por nosotros mismos, que se encarga de mapear los elementos de la UI que pueden cambiar y se lo comunica a la UI (a través de un Change Notifier Provider) y así esta se actualiza.

Esto nos ha dado flexibilidad de siempre saber qué elemento cambia y qué valor tiene en cualquier momento, lo cual ha sido valioso para fácilmente encontrar puntos de quiebre y poder corregir rápidamente.

Para ciertos escenarios particulares, usamos Interactors (casos de uso) que modelan incluso comportamientos que pueden reusarse. Caben perfectamente dentro de nuestra capa de lógica, y al ser artefactos individuales, pueden unitestearse, lo cual es sumamente relevante.

Un ejemplo de esto es cuando queremos darle favorito a un inmueble. ¿Lo podemos hacer dentro del ViewModel del detalle de cada uno, pero qué pasa si queremos utilizarlo en diferentes pantallas? (Por ejemplo, en una lista de búsqueda, o en un feed de real estates diferente que se muestra por los más vistos), implicaría repetir la lógica (copy+paste) en tantas partes como fuera a emplearse y si hay algún error o cambio, tendríamos que ir a cada uno, agregar diferentes tests, etc.

Con esta solución, lo centralizamos en una sola parte, ya está testada y puede mockearse para usarse en los diferentes ViewModel que lo necesiten.

Diferentes partes donde se puede seleccionar un favorito. Toda la lógica se es compartida desde un Interactor (“use case”)

3. Crear abstracciones para los paquetes/librerías de terceros

Es común que para utilizar algún paquete que encontramos en https://pub.dev/, hagamos copy + paste el ejemplo que nos proponen y adaptarlo a nuestro escenario y todo listo, seguimos implementando sobre eso. El problema, si lo hiciéramos siempre así, es que si nos cambia la firma o decidimos cambiar de librería, tenemos que tocar en tantas partes de nuestro código como hayamos pegado el llamado al paquete, y aparte de ser tedioso, repetitivo y carpintero, es un escenario perfecto para inyectar bugs que no existían.

¿Cómo lo manejamos? Muy fácil. Creamos un wrapper para cada paquete, por ejemplo, tenemos nuestro buscador de inmuebles implementado bajo el engine de Algolia y por decisión de negocio tenemos que reemplazarlo por otro motor. Creamos un wrapper para el buscador y apoyándonos del patrón de arquitectura Strategy logramos flexibilizar la implementación y poder usar cualquiera de las dos implementaciones en tiempo de ejecución. (Bueno, sabemos que no es tan plug & play, pero sí que nos ha sido supersimple ir abordando este ajuste).

En código sería algo como esto:

Nuestra clase abstracta
Nuestra implementación para Algolia
Nuestra implementación para un nuevo buscador

Ventajas:

  • Facilitamos desarrollar unit tests, porque los ViewModel’s dependen de nuestras abstracciones y no de terceros.
  • No paramos deploys a producción hasta que un feature esté completo, pues podemos hacer el cambio a la versión que si funciona mientras se termina la nueva implementación.
  • Centralizamos el conocimiento de que como utiliza ese paquete de un tercero en un solo lugar y cualquier miembro del equipo no tendrá que volver a aprender como funciona el tercero.
  • Si hay que corregir algún incidente, se corrige en el wrapper y queda corregido en todos las partes donde se emplee.

De forma similar, hemos empaquetado librerías para nuestra gestión de Analytics (Firebase, Segment, etc.) o con librerías usamos para widgets con efecto Shimmer o Feature discovery entre otras.

4. Documentar las buenas prácticas y algunas convenciones

En ocasiones, sobre todo cuando el equipo es nuevo o cuando llegan nuevos integrantes, algunas de las recomendaciones que solemos hacer al momento de hacer reviews de los PRs, tienden a ser repetitivas:

  • En el nombramiento de artefactos, de variables o de métodos.
  • Cuando queremos dejar evidencia de la forma en que lanzamos nuestros procesos de generación de versiones para staging o stores.
  • Cuando queremos tener un inventario de nuevos componentes que creamos y que pueden reutilizarse.
  • Cuando queremos discutir temas de mejoras técnicas de la aplicación (por ejemplo, cómo parsear y manipular mejor los deep links que abren la app).

Hemos estado probando diferentes herramientas para cubrir de la mejor manera estos escenarios, y hemos llegado a algunos acuerdos que no son una camisa de fuerza. Es claro para el equipo que para temas de nombramientos y convenciones lo hagamos en el Notion de la compañía, el cual, utilizamos como referencia en las revisiones de los PRs. Este documento está en constante actualizacion por parte de todos. Pasa similar con la documentación de nuestros procesos de automatización de despliegues.

Para los temas más técnicos y propios de la app, el módulo de discusiones de Github (nuestra plataforma para control de versiones y host de repositorios) ha sido de gran valor y ayuda porque ha permitido que todos los miembros técnicos puedan dejar sus comentarios, responder preguntas y votar por alguna respuesta. Y como todo cambia y estamos iterando continuamente, a veces revistamos respuestas que ya no aplican y reabrimos los debates.

Github Discussions

También los issues de Github nos ha permitido llevar control de issues técnicos que se salen de los issues de producto, y ha permitido que cada miembro del equipo se empodere de situaciones en las cuales por fallas técnicas algunos escenarios puedan estar afectando a nuestros usuarios. Según la severidad que el equipo mismo le dé a un tema específico, se aborda inmediatamente o se le da una gestión diferente.

Github issues

Todo lo anterior, nos ha ayudado a tener más consistencia en el proyecto, evitar discusiones infinitas o innecesarias y como es una construcción en equipo, todos somos dueños de ella, no invalidamos ideas ni generamos malos sentimientos de las personas.

5. Componetizar los features (Empaquetar por feature)

Si una pantalla de la app utiliza un botón que tiene el potencial de emplearse en varias pantallas más, entonces ese botón (que es un Widget) se convertirá en un componente con vida propia.

Supongamos el botón de Agendar Visita que tenemos en varias partes de la app. Permite, como su label indica, agendar una visita virtual de un inmueble con un asesor comercial de la compañía. En esta visita virtual se le presenta el proyecto a un cliente y desde el punto de vista técnico, tiene algo más que agendar la visita: trackea los eventos de analítica de intento de agendamiento, además del agendamiento en sí, además de las lógicas internas necesarias para llamar los servicios que agendan, actualizar perfil del usuario cuando se requiera, entre otros.

Para evitar que repliquemos toda esa lógica, creamos un componente/widget, que tiene su propio ViewModel con la lógica propia del agendamiento (y sus unit tests cubriendo los diferentes escenarios de uso).

Es importante que cada componente exponga sus dependencias en el constructor y así puede ser usado en tantas partes como sea necesario, sin tener que repetir toda la lógica, además de mantener la experiencia e interfaz de usuario consistente siempre.

Mismo Componente (Widget) de Agendar Visita usado en múltiples pantallas de la apliación

Similar a este botón tenemos el Widget del Comparador de propiedades, el botón de contacto vía WhatsApp con nuestros asesores, la galería de imágenes, el Widget de Reservas y Compras realizadas, entre muchos otros más.

Hay otra ventaja de tener componentes y es que si como equipo de producto queremos ver en qué pantalla es más eficaz un feature, podemos hacer A/B testing y reusar el componente que ya generamos en tales pantallas y medir.

6. Automatizar formateo del código y Code Style

En el día a día es común que al revisar un PR se vean cambios en partes del código que no están relacionados con la funcionalidad que estamos montando o que tengamos muchas líneas de código para revisar o que tengamos conflictos constantemente cada que generamos un PR, entre otros.

Para minimizar al máximo este tipo de situaciones y evitar desgastes del equipo innecesarios, se hace necesario definir y construir un Code Style y proveer al equipo herramientas que les permitan aplicarlo. Para garantizar que todos lo sigamos transparentemente y sin esfuerzos extras, decidimos crear:

  • Un script de git pre-push: se ejecuta cada vez que algún miembro del equipo hace un push de una rama, y parte de su funcionalidad es autoformatear todo el Codebase usando el Code Style definido. Así, si existe algún problema de formato, lo corrige y autogenera un commit con el ajuste. Este script debe configurarlo cada miembro del equipo en su configuración de Git y solo se hace una vez.
  • Linter/CodeStyle: lo configuramos basados en estándares de otras empresas agregando nuestras propias reglas en el tiempo.
  • Danger Systems: nos permite automatizar tareas comunes como Code Review, generar Changelogs automatizados basados en Conventional Commits y otros (como cantidad de cambios en un PR o mensaje de commits válido), señalizar con labels PRs muy grandes y difíciles de revisar, entre otros.

Logramos llevar a cabo buenas prácticas de PRs cada vez más granulares, mejorar nuestros labels y descripciones del PR siguiendo un formato genérico y hemos sido además de muy felices con esto, muy prácticos y veloces enfocándonos en tareas de codificación, más que de formateo.

Apuntes finales

Para finalizar, con este escrito hemos querido compartirles cuáles han sido estas buenas prácticas que para nuestro equipo han sido valiosas y han aumentado nuestra productividad mientras constantemente estamos iterando nuestro producto (Desarrollado en Flutter & Dart). Ha sido un tema evolutivo y que internamente en el equipo hemos discutido, propuesto, probado y aprobado.

Seguimos evolucionando, por ende el cambio seguro seguirá de alguna forma u otra afectándonos y algunas de estas prácticas puedan mejorarse, y otras abolirse para dar paso a nuevas.

Ahora la pregunta para ustedes:
¿Qué buenas prácticas de las mencionadas han usado? ¿Tienen alguna otra que no compartimos, pero que en sus equipos sí funciona?

--

--

Carlos Daniel
Building La Haus

Android & Flutter Developer. GDE for Android & Mobile Engineer.