Qué hemos aprendido de Flutter en La Haus, 1 año después de tener nuestra app en Stores

Carlos Daniel
Building La Haus
Published in
10 min readDec 17, 2021

Para nosotros dentro del equipo de desarrollo de apps, ha sido un verdadero reto y a la vez una real motivación encontrarnos con Flutter & Dart y su actual estado de desarrollo, pues nos ha permitido de una forma eficiente implementar las versiones Android y iOS de la app de clientes a la par que hemos disfrutado del proceso y las bondades tanto del lenguaje como de la plataforma. A hoy, tenemos suficientes certezas (para nuestro contexto) de que la decisión técnica de irnos por Flutter no solo fue apropiada sino de mucho beneficio a nivel técnico y de producto.

A continuación compartiremos algunas enseñanzas técnicas que nos ha dejado esta decisión, y que con el tiempo podríamos abordar y ajustar.

Uso un método o una clase para la implementación de mis widgets?

Ha sido tema de mucho debate a nivel de la comunidad, pero el manejo de refactorizar funciones de Widgets versus clases de Widgets también ha sido debate al interior de nuestro equipo. A qué nos referimos?

Teniendo por ejemplo un widget que muestre un error, podemos, o crear un método que retorne el Widget (si este widget es lo suficientemente grande puede reconstruirse incluso cuando nada al interior cambió)

O podemos crear una clase Widget, sobre la cual obtenemos el beneficio principal de que ésta solo se reconstruirá cuando el widget cambia, aprovechando el ciclo de vida del widget, mejorando el performance general.

Entonces, cuál de las 2 opciones es mejor? Como lo mencionábamos antes es un tema de discusión constante en la comunidad y recientemente el equipo de Flutter compartió un tweet con un video en el cual no seleccionan ninguna de las opciones como mejor pero nos dejan algunas ideas para seguir evaluando y midiendo.

Nosotros solemos usar ambas, cada una con sus usos particulares y hasta el momento nos ha ido bien; en la siguiente sección compartiremos cómo realizamos las mediciones para saber cuál es el widget más óptimo para nuestros escenarios.

Mediciones de la construcción widgets

Apalancados en las herramientas de performance de Firebase, hemos logrado tener medidas puntuales de pantallas y widgets de nuestra app, los cuales hemos ido afinando basados en los datos generados y así, el rendimiento de la app ha mejorado para beneficio de lo usuarios finales. La estrategia, a continuación:

Tenemos un widget (clase abstracta) que traza el tiempo que demora en construirse un widget stateful o stateless:

De dicha clase extienden los widget a los que queremos medir el performance, desde donde luego los llamamos de la siguiente manera:

Los datos van quedando disponibles en Firebase, en donde vamos analizando el comportamiento de algún widget en particular y tomamos decisiones de refactorizar o cambiar la implementación, o dejarla como está. Es una tarea continua de medir, ajustar, iterar.

Mejoras con el linter

Durante un año aproximadamente, nuestra base de código ha cambiado y variado lo suficiente para permitirnos mejorar su calidad, mantenibilidad, escalamiento y nuestra propia productividad. Nuestra herramienta de linter ha sido protagonista principal de esta mejora. Semanalmente con el equipo de desarrolladores mobile, nos reunimos e identificamos basados en el Dart Analysis del IDE, qué warnings podemos obviar o cuáles abordar, y a su vez, qué opciones podríamos introducir al analysis_options del linter para tratar un elemento como error o warning. Ello ha permitido que nuestros artefactos sean más limpios, tengamos muchos menos hints de análisis abiertos y que a su vez escribamos más limpiamente. La adición de const, el tratar los unused imports y variables como errores, entre otros, han sido mejoras que el equipo ha abordado exitosa y felizmente.

Extracto de nuestra configuración del linter — analysis_options.yaml

Y para complementar, tenemos igualmente definido un script pre-push el cual configuramos para que cada vez que hacemos push a nuestro código, valide que todos los unit tests se ejecuten exitosamente además de ejecutar las reglas de formato, lo cual nos ahorra el tener que lidiar con diferentes estilos de formato en el code base.

Make file para automatizar procesos de desarrollo

Durante muchos meses, fuimos bajo demanda creando diferentes scripts que nos sirvieran para trabajar con deep links, generar código, desinstalar una versión de la app de un dispositivo Android, reiniciar un device, correr los lints, o los tests, etc.

A medida que se fueron agregando más scripts, recordar el llamado para cada uno fue complicándose, o cuando necesitábamos ejecutar varios para una sola tarea, sea hacía tedioso. Hasta que decidimos crear un archivo make, que ahora nos da mayor flexibilidad y velocidad para automatizar todos estos procesos y flujos y los beneficios de tiempo y eficiencia son más que evidentes para el equipo.

Similar al analysis_options.yaml, este archivo sigue en constante evolución adaptada al crecimiento que siga teniendo nuestro proyecto y nuestro equipo de trabajo.

Una arquitectura base y clara

Al ser Flutter un framework declarativo, es mucho más simple de aprender y así ver resultados rápidamente de lo que necesitamos, pero también eso nos puede llevar a olvidarnos de reglas básicas de arquitectura, pues podríamos fácilmente mezclar responsabilidades entre capas y tener un super espagueti de cosas combinadas.

Para nuestra aplicación móvil, desde el comienzo definimos una arquitectura basada en principios SOLID además del patrón de diseño Model View -ViewModel - MVVM y el framework reactivo Mobius de Spotify, que nos permitió separar responsabilidades de la capa de UI del resto.

Si bien usamos Provider como nuestro manejador de estados, 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 (use case para algun@s) 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 view models, interactors y repositories.

Lo más importante es que nuestra arquitectura no es un tema fixed, al contrario, para nosotros está siempre en constante evolución y esta foto de hoy de ella, puede cambiar ya sea en una semana o en 3 meses o un año. Esta imagen es solo una guía visual de cómo está hoy:

Qué paquetes externos usamos?

Nuestra aplicación está basada en el uso de Google Maps, lo cual nos implica tener un acople de la misma a los paquetes provistos por Google/Flutter en este contexto. Similarmente, cuando hacemos uso de Google Street View, Firebase o el mismo Algolia para las búsquedas en el mapa.

Pero cuando hacemos uso de paquetes generales como los de renderización de SVGs, generación de código, carrusel de imágenes o caché de las mismas, lo que hacemos es seguir unas pautas bases como:

  1. No usar librarías obsoletas o que no tengan mucho seguimiento o ritmo de actualización periódica de parte de la comunidad
  2. Que sean populares, usualmente puntaje superior al 9x%
  3. Tienen buena gestión de issues? Si no, es un riesgo usar ese paquete.
  4. Ha venido actualizando la version de flutter con el tiempo? si no, puede ser un riesgo para no dejarte actualizar a nuevas versiones
  5. Caso similar, si no existe en el ecosistema Flutter algún paquete que haga lo que necesitamos dentro de nuestro contexto de app móvil, nosotros mismos implementamos como plugins y solemos hacerlos Open Source, tal como hemos hecho con el plugin de Segment para Flutter, o de Iterable para Flutter.
    NOTA: Recibimos igualmente apoyo y requests de features nuevos para ellos. :)

Una sola UI

Junto a los equipos de Product Management y Product Design, desde el principio decidimos tener (y aún nos sostenemos en esa decisión debido a los buenos resultados que quedan después de un año de user research) una única UI para la app, evitando tener conversaciones sin sentido de cada plataforma y así enfocarnos en el desarrollo del producto. Por tanto, nuestras UI están diseñadas teniendo presente una experiencia común independiente de si la app es usada en un dispositivo Android o iOS, sacando lo mejor de ambos.

Imagen tomada del Apple Store
Imagen tomada de Google Play

Estrategia de Monitoreo

Solo mencionaremos de momento, que el monitoreo dentro de nuestros procesos de desarrollo ha sido uno de los grandes protagonistas durante los últimos meses, pues gracias al uso de Crashlytics y su estrategia de breadcrumbs o más recientemente de Sentry hemos podido de una forma más integral el poder monitorear todos los servicios y features que tenemos en nuestra app de clientes. Esto nos ha ayudado a mantener un porcentaje promedio superior al 99% de usuarios libres de crashes, dado a que podemos identificar casuísticas de errores puntuales en dispositivos que estamos constantemente evaluando y corrigiendo. Parte de nuestras actividades dentro de nuestra planeación de sprints incluyen el estar monitoreando estos escenarios y mejorando la forma en que lo hacemos.
En una próxima entrega, profundizaremos más en el tema.

Haciendo nuestra app responsive

Nunca consideramos desde el principio poder tener soporte adicional a pantallas de celular para nuestra app de clientes. Pero nos fuimos dando cuenta según los datos arrojados por los monitoreos en Firebase y Sentry que una importante base de usuarios han estado usando la app desde tablets, principalmente desde iPads. Por ello, desde producto e ingeniería tomamos la decisión de darles soporte y así hemos ido haciendo nuestra app responsive de una forma progresiva.

Versión tablet No Responsive de lista de propiedades de interés
Versión responsive en tablet de propiedades de interés

Además, luego de que en dispositivos Mac Book Pro M1 pueda usarse nuestra app en desktop (instalación habilitada desde la aplicación del App Store), el principal beneficiario de esto es nuestro equipo comercial, pues pueden ya tener acceso desde su computador de trabajo no solo a las herramientas relacionadas con sus labores de venta y servicio post venta sino también del uso de la app en sus estaciones de trabajo, lo cual ha mejorado su productividad al tener acceso a la aplicación de clientes en el mismo dispositivo.

Estrategia de CI

Nuestra estrategia de CI está basada en una mezcla más que interesante entre Fastlane y Bitrise, desde el punto de vista de los retos técnicos que hemos enfrentado al momento de integrar continuamente aplicaciones híbridas. Por ejemplo, nosotros no hacemos build de cada plataforma cada que creamos un PR, pues el proceso es lento y costoso. Por ello decidimos realizarlo/configurarlo al momento de hacer merge con la rama master justamente para hacer más eficiente este proceso.

Así entonces, hay que aclarar que cuando queremos desplegar la versión Android de la app, todas las configuraciones que hacemos no pueden replicarse para cuando queremos desplegar para iOS, pues son procesos diferentes y robustos, y para disminuir esta brecha de configuraciones es cuando Fastlane nos da muchísimo valor, pues nos permite crear componentes de acciones que pueden compartirse entre ambos procesos de despliegue además de documentar qué es posible y qué no en un lenguaje común.

Definición directa desde Fastlane

Adicional, paulatinamente hemos ido integrando también algunos actions de Github dentro de nuestro proceso de generación de versiones (no directamente en el CI), que nos ha permitido tener un control más granular por ejemplo de los temas afines a validación de nombres en PRs, que luego nos permitan generar un archivo de changelog de forma dinámica cuando queremos generar nuestras versiones para Stores. (Más detalles de esto en un siguiente post describiendo el proceso completo 🤓 )

danger.yaml en configuración de action de Github para validación de nombramiento de PR

Pensamientos Finales

No ha sido hasta el momento el viaje más fácil, pero tampoco ha sido algo imposible y que nos haya generado mucho conflicto de desarrollo e implementación, pero hemos enfrentado retos importantes que han permitido evolucionar nuestra app de clientes de la mejor forma.

En un año pueden pasar muchas cosas y en este pequeño resumen intentamos condensar lo mejor que nos ha pasado además de compartirles algunas de las buenas prácticas que hemos ido adoptando. Seguimos sumando experiencias y compartiéndolas con tod@s para mejorar la comunidad Flutter.

Gracias a Julian Sotelo, Daniel Gómez Rico y Santiago Orozco Guayara por agregar contenido a este articulo :)

No olviden descargar nuestra app para que compres finca raíz a tu ritmo!

--

--

Carlos Daniel
Building La Haus

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