Nuestra aplicación Android está a dieta: Implementando Dynamic Features
¿Cómo se implementa un Dynamic Feature en esta escala? ¿Cómo es nuestra arquitectura? ¿Qué cambiamos? ¡Aprende todo esto y mucho más!
Bienvenidos y bienvenidas de nuevo, ¿cómo están? En este post continuaremos con la segunda parte de nuestra historia con Dynamic Features, esperemos que estén disfrutando de este proceso como lo hicimos nosotros.
Recuerden que para comprender esta lectura es necesario que primero aprendas de nuestra primera historia y puedas llevarte un mejor entendimiento general, donde estuvimos viendo la definición de Dynamic Features, qué podemos lograr con esta herramienta, qué tipos de instalaciones dinámicas nos ofrece Google entre otras cosas.
Dicho esto, comencemos.
Antes de empezar el desarrollo, nos hicimos algunas preguntas que fuimos respondiendo a medida que avanzamos con el análisis:
A nivel de arquitectura…
Nuestras aplicaciones ya estaban divididas por módulos y a partir de aquí nos hicimos las siguientes preguntas:
¿Qué cambios tenemos a nivel de las apps de ML & MP?
A nivel de las apps, comenzaremos a tener módulos dinámicos a medida que los equipos comienzan a sumarse a dynamic features.
Necesitamos establecer una arquitectura de proyecto para que sea escalable, así que el primer paso fue definir esta arquitectura a nivel proyecto dentro de nuestras aplicaciones.
¿Qué pasa si navegamos a un módulo dinámico desde una push / mail o alguna forma externa?
Necesitamos crear una librería que actúe de wrapper para hacer este manejo, esa fue la solución más escalable que encontramos y que además no solo nos permite controlar la navegación sino que además veremos otras ventajas sobre tener este wrapper.
Se ejecuta el flujo que definimos en nuestra librería el cual valida si el módulo se encuentra o no descargado, si no lo tenemos lo descarga y navega.
¿Qué pasa si nuestro módulo necesita una configuración inicial antes de iniciarse?
Para este caso definimos una arquitectura que nos permita inyectar la configuración necesaria sobre el módulo una vez que este ha sido instalado dentro del wrapper que creamos.
A nivel de proceso de release…
¿Estos cambios modifican el proceso de Release actual que tenemos?
No, el proceso continúa siendo de la misma forma que lo tenemos hoy, al generar el bundleRelease, el empaquetado ya contiene dentro el / los módulos dinámicos con sus configuraciones, y PlayStore es el encargado de hacer la instalación de cada uno de ellos, basado en sus configuraciones cuando corresponda.
¿Qué pasa cuando hacemos un update de la aplicación?
Necesitamos tener presente que cualquier update que se suba al store, ya sea que fuese un update en un cambio de la app base o un cambio que sólo afecte al módulo dinámico, siempre será un bundle de la aplicación entera, por lo cual, ya sea que el usuario tuviera o no una versión con dynamic features, va a ver el update disponible.
Referencia sobre delta updates
A nivel de manejo de dependencias…
¿Qué sucede si tanto el módulo base como el dinámico comparten una misma dependencia y se actualiza la misma sólamente en uno de ellos?
Se toma la versión definida en la app base, salvo que especifiquemos con algún mecanismo de force lo contrario desde el módulo dinámico, lo cual no deberíamos hacer para no generar inconsistencias en el flujo.
¿Qué pasa en la app cuando agregamos más de un módulo dinámico?
De acuerdo a la arquitectura de Dynamic Features, todos los módulos dinámicos dependen directamente del módulo base, en este caso de nuestras apps.
Esto puede traer problemas con dependencias, por ejemplo si alguno de los módulos que tiene la app depende de alguna librería que venga con diferentes versiones a nivel app y a nivel módulo dinámico.
Para estos casos hay que forzar la versión de esa librería en el módulo base (en nuestras apps) para evitar estos conflictos.
A nivel general nos quedaba…
¿Qué sucede si borramos el caché y eliminamos los datos guardados de nuestras apps?
No pasaría nada, el módulo dinámico una vez descargado, ya forma parte de la aplicación general en conjunto, por lo cual continuará con el módulo descargado y no deberá volverlo a descargar.
¿Qué pasa si compilamos por consola con assembleRelease o assembleDebug?
Para estos casos, no vamos a disponer del módulo dinámico ya que el mismo no forma parte de los empaquetados de assemble.
Para probarlo en release usamos bundleRelease, y para usarlo en debug usamos bundleDebug.
Luego de responder estas preguntas, comenzamos con una POC en 2018/19 donde para lograr migrar un módulo a dinámico obtuvimos los siguientes resultados:
- Los recursos externos utilizados dentro del módulo dinámico deben ser referenciados con el full path, caso contrario no los encontrará la aplicación en runtime produciendo un crash de la misma.
- Es una implementación que si bien puede adaptarse a otros stores, requiere mucho más trabajo, por lo que hoy en día usamos la implementación solo adaptada a Google PlayStore.
- Para la navegación externa el único manejo posible es interceptar la misma y manejar la instalación / navegación al módulo a través de un wrapper.
- En un principio la desinstalación del módulo no funcionaba, y aunque hoy funciona, no es algo que podemos controlar, no sabemos cuándo pasa ni tenemos un callback del evento en cuestión.
Avanzamos en el desarrollo y mejoras de alguno de estos puntos en conjunto con Google para poder acercarnos a la posibilidad de llevar un desarrollo productivo de este feature, y en ese tiempo mientras dejamos el feedback a Google dejamos la iniciativa en stand by.
Retomamos en 2020 el desarrollo ya que en este interín se habían resuelto alguno de los puntos mencionados anteriormente y pasamos a definir cuándo deberíamos tener un módulo dinámico:
¿Cuándo nuestro módulo debería ser un Dynamic Feature?
Es importante entender cuándo nuestro flujo debería ser parte de un flujo dinámico y cuando no. Hay algunos factores que nos ayudan a determinar cuándo deberíamos tener un flujo que se ejecute a demanda por parte del usuario o que se ejecute a demanda en background en algún momento específico en base a alguna acción.
Siempre tengamos en cuenta que el flujo debe estar modularizado.
Condiciones Generales
Detallamos algunas condiciones generales para entender cuándo nuestro flujo debería ser dinámico para una mejor experiencia:
- Si nuestro flujo no está habilitado a todos los países de la aplicación
- Si nuestro flujo se prende / apaga desde backend y no está siempre disponible sino que bajo ciertas condiciones
- Si nuestro flujo tiene muy poco uso por parte del usuario final (registrarme en la app) o que sea algo que no se ejecute muchas veces.
- Si nuestro flujo necesita de dependencias de terceros que agregan un peso elevado al integrarnos con la app principal (X%).
¿Cómo cambió nuestra arquitectura?
En este momento tomamos la decisión de llevar un módulo pequeño como primera interacción con Dynamic Features de forma productiva, mediante el tipo de instalación onDemand.
Para esto nuestra aplicación pasó de ser un contenedor de dependencias a tener módulos dinámicos “separados”, generando una dependencia desde el módulo dinámico a la aplicación base, y dejando una referencia dentro de la app base al módulo dinámico, como muestra Google en su documentación.
Pasamos de esta arquitectura :
A esta otra:
También desarrollamos el wrapper que mencionamos al principio, el cual nos dió los siguientes beneficios:
Podemos tener control sobre la navegación que queremos manejar, tanto interna como externa, para una mejor experiencia en general.
- Tanto la navegación interna como externa se resuelve en el wrapper de Dynamic Features que tenemos, el cual se encarga de validar si necesitamos descargar el módulo, si ya lo tenemos descargado y sólo basta navegar, si el módulo a descargar existe, además de mantener retrocompatibilidad con los deeplinks “antiguos” para que los equipos no necesiten migrar la entrada a sus flujos actuales para hacer su módulo dinámico, entre otras cosas.
- Iniciar módulos que necesiten configuraciones iniciales al instalarse.
- Tenemos la posibilidad de darle un feedback visual al usuario final personalizado, lo cual nos da la opción de iterar y seguir mejorando en base a las buenas prácticas que nos provee la documentación oficial de Android y las validaciones con el equipo de UX.
- Podemos mantener métricas sobre varios puntos de nuestro flujo dinámico como por ejemplo:
- ¿Cuántos usuarios se descargan el módulo dinámico?
- ¿Cuántos usuarios navegan activamente al módulo dinámico?
- ¿Cuántos usuarios tienen errores al descargar un módulo dinámico?
- Tenemos distinciones para saber qué tipo de errores puede tener el usuario y si se recupera de los mismos.
- Cuánto demora la descarga de un módulo dinámico
Al mismo tiempo quisimos ir un poco más allá de lo que Google nos ofrecía, y armar un nuevo tipo de instalación que se adaptara al flujo que tenemos dentro de nuestras aplicaciones.
De esta manera es como nació… Background
Este modo de distribución es creado por nosotros de manera custom para poder descargar un módulo dinámico sin que el usuario reciba feedback de la operación que se está realizando y sea transparente la experiencia para cuando necesite acceder al mismo.
Luego de estos cambios, necesitábamos probar muy bien nuestro flujo para ver que todo seguía funcionando correctamente.
De estas pruebas pudimos obtener más datos interesantes, basándonos en que nuestro flujo de background era reinventado por nosotros, vimos principalmente que teníamos errores de instalaciones cuando la aplicación se encontraba en background.
De esta forma tuvimos que adaptar las instalaciones en background a eventos de foreground desde acoplarnos al ciclo de vida de las actividades hasta de los flujos de registración, terminando por optar que la mejor manera de manejarlo es en conjunto con la inyección de configuraciones que realizamos cada vez que la app despierta, es decir cuando se inicia en foreground, y de esta manera dejar los flujos que se instalen en background como parte de ese proceso.
¿Cómo probamos nuestro módulo dinámico?
Tenemos dos opciones para probar nuestro flujo dinámico, una es con bundletools de forma local y la otra es a través de internal app sharing.
Para la prueba Local pueden leer la documentación oficial de Android, y para las pruebas con internal app sharing es necesario subir un bundleRelease de nuestra aplicación ya que la consola de Google valida que el empaquetado esté firmado de manera oficial.
Un buen tip al momento de necesitar hacer debug sobre un compilado dinámico es subir un bundleDebug con el package id de nuestra aplicación en release (que no incluya el .debug), de esta manera podemos subir a internalSharing el empaquetado en debug y podemos descargarlo y debuguear sobre el mismo.
Resumimos entonces…
Hasta ahora no tenemos cambios en nuestro proceso de release pero tuvimos que adaptar la arquitectura de proyectos para escalar con varios módulos dinámicos definidos a nivel de las apps al mismo tiempo que diseñar de forma escalable un wrapper que nos permita manejar la navegación y nos ofreció varias ventajas sobre tener tracks y dashboards que nos permitan tener visibilidad de la estabilidad de los módulos que forman parte de esta experiencia; además de permitirnos tener una arquitectura que permite configuraciones iniciales para los módulos que lo necesiten antes de iniciar sus flujos.
Al mismo tiempo fuimos un paso más allá para tener un nuevo tipo de instalación en background que nos permitiera un abanico más grande al momento de que un equipo necesite sumarse a ser dinámico pensando en la mejor experiencia para el usuario final.
En conjunto con todo este desarrollo le dedicamos mucho tiempo a las pruebas para poder continuar mejorando la experiencia principalmente pensando en los equipos que se irían sumando a esta nueva iniciativa, con el objetivo de tener una solución escalable que se mantenga estable y sea amigable para los desarrolladores.
¿Cómo continuamos luego de este desarrollo ?
Luego de tener nuestro wrapper desarrollado y nuestro primer módulo en producción, avanzamos con medir los resultados y definir nuestros siguientes pasos:
- ¿Cómo adaptamos este nuevo flujo dinámico a las herramientas internas de test que tenemos?
- ¿Cómo nos fue con el peso de la aplicación a medida que los equipos se fueron sumando a esta experiencia?
- ¿En qué estamos trabajando actualmente en esta iniciativa?
Para conocer estas respuestas te invitamos a leer la última entrada de nuestra serie de historias sobre incorporar Dynamic Features, esperamos que la estés disfrutando :)