Cómo modularizamos una aplicación Android

¡Si no pudiste asistir al evento del día 23 de febrero y quieres saber qué es la Modularización, éste es tu artículo!

¿Qué es modularizar?

Si hablamos de modularizar nos referimos a la práctica de organizar nuestra base de código en pequeñas partes o módulos que deben contener las siguientes características:

  • Acoplamiento bajo: los módulos deben de ser lo más independientes entre sí, lo cual quiere decir que no deben de conocer el funcionamiento interno del resto.
  • Cohesión alta: los módulos deben de ser lo más independientes posibles y funcionar como sistemas únicos.

¿Por qué deberíamos modularizar?

Es lo primero que debemos preguntarnos a la hora de comenzar un proyecto, y con estos puntos que detallaremos a continuación nos haremos una idea de porqué hacerlo.

  • Reutilización: un módulo independiente puede ser usado en múltiples aplicaciones
  • Control de visibilidad estricto: podemos controlar de una manera fácil qué exponer fuera del módulo. Lo ideal es exponer mediante interfaces la funcionalidad del módulo de cara al exterior.
  • Mantenibilidad: al tener una base de código más pequeña y con funciones más definidas es mucho más legible y comprensible. Además, al tener su responsabilidad mucho más acotada evitamos la posibilidad de sufrir efectos colaterales al realizar cambios en él.
  • Testing: tener menos código y que este sea más conciso nos permite realizar test de una manera más fácil.
  • Tiempo de compilación: tras realizar cambios en un módulo, solo este y aquellos que dependan de él volverán a compilarse, esto se traduce en tiempos de compilación más cortos. Además, podemos configurar Gradle para aprovechar algunas funcionalidades avanzadas como veremos más adelante.
  • Gestión de dependencias: cada módulo tendrá una serie de dependencias internas, las cuales no serán expuestas al exterior, esto nos permite diseñar un comportamiento exclusivo para él sin que tengamos necesidad de saber cómo está implementado.
  • Entrega personalizable: En el caso particular de Android, existe “Play Feature Delivery” lo cual permite ofrecer funciones de la aplicación bajo demanda. Para poder usar esta funcionalidad el proyecto debe de estar perfectamente dividido en módulos, más adelante profundizaremos en este punto.

¿Qué deberemos tener en cuenta a la hora de modularizar?

Como toda decisión que tomemos, deberemos tener en cuenta una serie de factores a la hora de modularizar nuestro código.

  • Deberemos tener una configuración coherente de los módulos:

- Si tenemos una gran cantidad de módulos pequeños en los que vemos que nuestro código se repite sin apenas variación quizá esta separación no tenga sentido y debamos plantearnos unificarlos.

- Por el contrario, si nuestros módulos son demasiado grandes y se convierten por sí mismos en aplicaciones monolíticas deberemos plantearnos el dividirlos.

  • Gestión de la configuración de dependencias de Gradle: algo importante a la hora de gestionar las dependencias de nuestros módulos es centralizarlas en un único punto, si tenemos un fichero que las contenga nos será mucho más fácil tener control sobre ellas.
  • Evitar un alto nivel de complejidad: habrá que evaluar nuestro proyecto y su alcance, ¿el proyecto va a crecer mucho? ¿va a ser algo simple? Seamos razonables, no hay motivo para aplicar sobreingeniería.
  • Gestión de la navegación entre funcionalidades: ya que los módulos no se conocen entre sí, la navegación adquiere complejidad, aunque tenemos formas de gestionarla, ya sea mediante librerías como por ejemplo Android Jetpack Navigation mediante una interfaz que comparten todos los módulos y cuya implementación se encuentre en el módulo de "APP".
  • Inyección de dependencias: aunque las librerías nos abstraen de gran parte del trabajo deberemos tener ciertos conocimientos para saber cómo usarla para el manejo de nuestros módulos.
  • Dependencias circulares: este aspecto a tener en cuenta trata de la posibilidad de que existan dependencias entre 2 módulos entre sí

Play Feature Delivery

Antes comentamos esta característica y ahora vamos a entrar un poco en detalle con ella.

Por hacer un poco de memoria, recordemos que Google Play hace uso de los Android App Bundle o AAB para generar APK personalizados para la configuración del dispositivo de cada usuario, de tal manera que se descarguen solo los recursos y código necesarios para la ejecución de la app.

Play Features Delivery va más allá y permite que ciertas funcionalidades de la APP se entreguen de manera condicional o a petición. Para poder conseguir esto lo primero que debemos hacer es tener dichas funciones separadas en módulos.

Esto ofrece algunas ventajas como la mejora de la velocidad de compilación o de ingeniería, además de la mejora del tamaño de la aplicación. Aunque también trae consigo algunas limitaciones que se deberán tener en cuenta y que son las siguientes:

  • Instalar una gran cantidad de módulos puede provocar problemas de rendimiento
  • Se debe limitar la cantidad de módulos como extraíbles en el momento de la instalación a 10 o menos. De lo contrario, el tiempo de descarga e instalación de tu app podría ser alto.
  • Esta funcionalidad está disponible sólo en Android 5.0 (nivel de API 21) y versiones posteriores.
  • Se debe habilitar SplitCompat para que la app tenga acceso a los módulos de las funciones.
  • Dado que la entrega de funciones requiere que publiques tu app con un paquete de aplicación, asegúrate de tener en cuenta los problemas conocidos para estas funcionalidades.

Comunicación entre módulos

Bien, hasta este punto tenemos claro que los módulos han de ser independientes y no saber nada entre ellos, pero tendrán que compartir información para poder trabajar juntos, ¿no?

Existen varías estrategias que podemos usar para solucionar este punto y que veremos a continuación:

  • Usar un módulo de nexo: en este caso existiría un intermediario que podrá escuchar los mensajes entre ambos módulos y reenviarlo según las necesidades. Por ejemplo, imaginemos que tenemos que pasar información entre la pantalla A y la pantalla B y que ambas se encuentran en diferentes módulos. En este caso, el mediador posee un grafo de navegación en el que podemos pasar la información necesaria para poder continuar con el flujo
  • Usa identificadores: como no es del todo correcto pasar un objeto completo como argumento de navegación, el módulo de destino podría recibir un identificador de la información necesaria y podríamos acceder a ella mediante un caso respetando así el single source of truth.

Algunas recomendaciones generales

Llegados a este punto verás que no hay una única manera de modularizar, al igual que existen diferentes arquitecturas para nuestra aplicación, existen muchas maneras de dividir nuestro código, en lo que sí podemos estar de acuerdo es que hay que tener en cuenta los siguientes puntos para tener un código legible y mantenible:

  • Mantener una configuración coherente: es importante tener en cuenta que si tenemos dependencias comunes deberemos tener sus versiones recogidas en un único punto de tal manera que nos sea fácil gestionarlas al igual que la configuración común que pueda existir.
  • Exponer lo menos posible: la interfaz expuesta por un módulo debe ser lo más pequeña posible, no se debe conocer los detalles de la implementación, para ello es importante usar la propiedad private o internal que nos proporciona Kotlin. A la hora de implementar tus dependencias intenta usar implementation en lugar de Api, la primera solo permite usar la dependencia en el módulo que la implementa, mientras que la segunda, además, en todos los módulos que consuman el módulo que la implemente, cuidado con esto. Esto además impacta directamente en los tiempos de compilación.
  • Módulos Kotlin y Java: cuando se generen nuevos módulos opta por esta opción, nos darán muchas ventajas respecto a otros tipos, como por ejemplo que no contienen recursos de Android, assets o ficheros de manifest, haciéndolos más ligeros.

Tipos de Modularización

Tras todo lo hablado sobre modularización, es buen momento para echar un vistazo a los tipos existentes, esto nos permitirá elegir cuál es la más conveniente para nuestro proyecto.

  • Modularización única o monolítica: es aquella en la que todo el código se encuentra junta, es decir, la lógica de presentación, negocio y datos está en un solo módulo. Para una aplicación pequeña que no vaya a crecer en el tiempo o en funcionalidades es perfectamente aceptable y válida. Si empezara a escalar podríamos tener problemas y sería aconsejable plantearnos el migrar a otro tipo de modularización.
  • Modularización por capas: es la recomendada por Google en sus guías de arquitectura y es el más usado. Además, es el modelo que seguimos en nuestros proyectos en EDEX aunque con algunas modificaciones.

Se divide en 2 capas básicas, UI y Data, siendo Domain opcional, teniendo cada una de ellas una responsabilidad específica

Veamos qué hace cada una de ellas:

  1. La capa UI es la encargada de presentar la información y permitir la interacción del usuario con la aplicación. Se encarga de definir la estructura y el diseño de la interfaz de usuario, así como de la gestión de eventos y las navegaciones entre las vistas. La separación de la capa UI de las capas de negocio y de datos es muy importante, ya que permite mantener una clara separación de responsabilidades y un alto grado de cohesión. Además, esta separación permite una mayor escalabilidad y mantenibilidad del proyecto, ya que los cambios en la interfaz de usuario no afectan a la lógica de negocio ni a las operaciones de acceso a datos.
  2. La capa DOMAIN es el nexo entre la capa UI y la capa DATA. Es responsable de comunicar peticiones y resultados entre las capas de UI y DATA a través de casos de uso, así como de albergar clases que podemos usar como modelos en nuestra aplicación.
  3. La capa de DATA sería responsable de gestionar la lógica de negocio y de acceder a las fuentes de datos.

Por poner un ejemplo de la importancia de tener una buena modularización imaginemos que tenemos una aplicación con el sistema de vistas clásico en XML y decidimos cambiarlo a Compose, si todos nuestros módulos están correctamente separados y no existe acoplamiento entre ellos con cambiar el módulo de UI por el nuevo módulo con Compose sería suficiente, ¿interesante verdad? Intenta imaginar esta misma situación con una aplicación monolítica, la cosa se complicaría.

  • Modularización por Características o Features: en este caso la separación se realizaría por secciones, por ejemplo, el Login, el registro, los pagos… Este tipo de separación es muy útil cuando el equipo de desarrollo es grande y trabaja por ejemplo en squads. Los tiempos de compilación se reducirían ya que realizarían los cambios en un solo módulo. Además, garantizamos una alta cohesión ya que las responsabilidades están muy claras.

Por el contrario, habría que tener cuidado con las dependencias que pudiera haber entre módulos, ya que podría existir la necesidad de compartir información entre ellos.

  • Modularización mixta: es una mezcla de las vistas anteriormente, es decir, cada característica tendría internamente una separación por capas. Este tipo nos ofrece una alta cohesión ya que los módulos poseen una responsabilidad bien definida y bajo acoplamiento, lo cual hace muy fácil la escalabilidad.

También existen algunos problemas

El proyecto puede volverse complejo ya que hay que administrar múltiples módulos y gestionar sus configuraciones, además es conveniente tenerlos actualizados en la medida de lo posible, lo cual genera un esfuerzo adicional para que todos los módulos estén siempre funcionando correctamente de manera conjunta.

Se recomienda su uso en proyectos grandes con muchos desarrolladores.

Con esto terminamos este artículo, como verás no hay ni mejores modularizaciones ni peores, como desarrolladores, deberemos evaluar el proyecto y las necesidades de este, para poder tomar una decisión según los pros y contras de cada una de ellas y usar la que más se ajuste a él.

vídeo Android

 

Tags

He leído y acepto la política de privacidad
Acepto recibir emails sobre actividades de recruiting NTT DATA