A fines de febrero, lanzamos un nuevo formato para reuniones de desarrolladores de Android de Kaspersky Mobile Talks . La principal diferencia con respecto a las manifestaciones ordinarias es que, en lugar de cientos de oyentes y presentaciones hermosas, los desarrolladores "experimentados" se reunieron en varios temas diferentes para discutir un solo tema: cómo implementan la multimodularidad en sus aplicaciones, qué problemas enfrentan y cómo los resuelven.

Contenido
- Antecedentes
- Mediadores en HeadHunter. Alexander Blinov
- Módulos de dominio Tinkoff Vladimir Kokhanov, Alexander Zhukov
- Análisis de impacto en Avito. Evgeny Krivobokov, Mikhail Yudin
- Al igual que en Tinkoff, redujeron el tiempo de montaje para relaciones públicas de cuarenta minutos a cuatro. Vladimir Kokhanov
- Enlaces utiles
Antes de pasar al contenido inmediato de la reunión en la oficina de Kaspersky Lab, recordemos de dónde vino el mod para dividir la aplicación en módulos (en adelante, el módulo se entiende como un módulo Gradle, no una Daga, a menos que se indique lo contrario).
El tema de la multimodularidad ha estado en la mente de la comunidad de Android durante años. Uno de los fundamentales puede considerarse un informe de Denis Neklyudov en el "Mobius" de San Petersburgo del año pasado. Propuso dividir la aplicación monolítica, que había dejado de ser un cliente ligero, en módulos para aumentar la velocidad de construcción.
Enlace al informe: presentación , video
Luego hubo un informe de Vladimir Tagakov de Yandex.Maps sobre la vinculación de módulos con Dagger. Por lo tanto, resuelven el problema de asignar un solo componente de tarjetas para su reutilización en muchas otras aplicaciones Yandex.
Enlace al informe: presentación , video
Kaspersky Lab tampoco se mantuvo al margen de la tendencia: en septiembre, Evgeni Matsyuk escribió un artículo sobre cómo conectar módulos usando Dagger y al mismo tiempo construir una arquitectura de múltiples módulos horizontalmente, sin olvidar seguir los principios de la Arquitectura limpia verticalmente.
Enlace al articulo
Y en el invierno de Mobius hubo dos informes a la vez. Primero, Alexander Blinov habló sobre la multi-modularidad en la aplicación HeadHunter usando Toothpick como DI, y justo después de él, Artem Zinnatulin habló sobre el dolor de más de 800 módulos en Lyft. Sasha comenzó a hablar sobre la modularidad múltiple, como una forma de mejorar la arquitectura de la aplicación, y no solo acelerar el ensamblaje.
Informe Blinov: Presentación , Video
Informe Zinnatulin: Video
¿Por qué comencé el artículo con una retrospectiva? En primer lugar, le ayudará a estudiar mejor el tema si está leyendo sobre la modularidad múltiple por primera vez. Y en segundo lugar, el primer discurso en nuestra reunión comenzó con una mini-presentación de Alexei Kalaida de la compañía Stream, que mostró cómo dividieron su aplicación en módulos basados en el artículo de Zhenya (y algunos puntos me parecieron similares al enfoque de Vladimir).
La característica principal de este enfoque fue el enlace a la interfaz de usuario: cada módulo está conectado como una pantalla separada, un fragmento al que se transfieren las dependencias desde el módulo de aplicación principal, incluido el FragmentManager. Primero, los colegas trataron de implementar la multimodularidad a través de inyectores proxy, que Zhenya propuso en el artículo. Pero este enfoque parecía abrumador: había problemas cuando una característica dependía de otra, que, a su vez, dependía de la tercera: teníamos que escribir un inyector proxy para cada módulo de características. El enfoque basado en componentes de UI le permite no escribir ningún inyector, lo que permite dependencias en el nivel de dependencia de los fragmentos de destino.
Las principales limitaciones que tiene esta implementación: una característica debe ser un fragmento (u otra vista); La presencia de fragmentos anidados, lo que conduce a una gran repetitiva. Si una característica implementa otras características, debe agregarse al mapa de dependencias, que Dagger comprueba al compilarla. Cuando existen muchas de estas características, surgen dificultades al momento de vincular el gráfico de dependencia.
Después del informe de Alexey, Alexander Blinov tomó la palabra. En su opinión, la implementación vinculada a la interfaz de usuario sería adecuada para contenedores DI en Flutter. Luego, la discusión cambió a una discusión de varios módulos en HeadHunter. El propósito de su división en módulos era la posibilidad de aislamiento arquitectónico de las características y aumentar la velocidad de ensamblaje.
Antes de dividirse en módulos, es importante prepararse. Primero, puede crear un gráfico de dependencia, por ejemplo, utilizando dicha herramienta . Esto ayudará a aislar los componentes con un número mínimo de dependencias y eliminar los innecesarios (chop). Solo después de esto, los componentes menos conectados se pueden seleccionar en módulos.
Alexander recordó los puntos principales sobre los que habló con más detalle en Mobius. Una de las tareas complejas que la arquitectura debe tener en cuenta es reutilizar un módulo desde varios lugares de la aplicación. En el ejemplo con la aplicación hh, este es un módulo de currículum, que debe ser accesible tanto para el módulo de lista de vacantes (VacanciesList), cuando el usuario va al currículum que envió para esta vacante, como al módulo de respuesta negativa (Negociación). Para mayor claridad, volví a dibujar la imagen que Sasha representaba en un rotafolio.

Cada módulo contiene dos entidades principales: dependencias, las dependencias que este módulo necesita, y API, los métodos que el módulo proporciona a otros módulos. La comunicación entre los módulos se lleva a cabo por mediadores, que son una estructura plana en el módulo de aplicación principal. Cada característica tiene una selección. Los propios mediadores están incluidos en un determinado MediatorManager en el módulo de aplicación del proyecto. En código, se parece a esto:
object MediatorManager { val chatMediator: ChatMediator by lazy { ChatMediator() } val someMediator: ... } class TechSupportMediator { fun provideComponent(): SuppportComponent { val deps = object : SuppportComponentDependencies { override fun getInternalChat{ MediatorManager.rootMediator.api.openInternalChat() } } } } class SuppportComponent(val dependencies) { val api: SupportComponentApi = ... init { SupportDI.keeper.installComponent(this) } } interface SuppportComponentDependencies { fun getSmth() fun close() { scopeHolder.destroyCoordinator < -ref count } }
Alexander prometió publicar pronto un complemento para crear módulos en Android Studio, que se utiliza para deshacerse de copiar y pegar en su empresa, así como un ejemplo de un proyecto de consola de módulos múltiples.
Algunos datos más sobre los resultados actuales de la separación del módulo de aplicación hh:
- ~ 83 módulos de características.
- Para realizar una prueba A / B, las características se pueden reemplazar completamente por el módulo de características en el nivel de mediador.
- El gráfico de Gradle Scan muestra que después de la compilación de los módulos en paralelo, se lleva a cabo un proceso bastante largo de desintoxicación de la aplicación (en este caso, dos: para solicitantes de empleo y empleadores):

Lo siguiente tomó la palabra de Alexander y Vladimir de Tinkoff:
El esquema de su arquitectura de módulos múltiples se ve así:

Los módulos se dividen en dos categorías: módulos de características y módulos de dominio.
Los módulos de características contienen lógica de negocios y características de la interfaz de usuario. Dependen de los módulos de dominio, pero no pueden depender el uno del otro.
Los módulos de dominio contienen código para trabajar con fuentes de datos, es decir, algunos modelos, DAO (para trabajar con la base de datos), API (para trabajar con la red) y repositorios (combinan el trabajo de la API y DAO). Los módulos de dominio, a diferencia de los módulos de características, pueden depender unos de otros.
La conexión entre el dominio y los módulos de características tiene lugar completamente dentro de los módulos de características (es decir, en la terminología de hh, las dependencias y las dependencias API de los módulos de dominio se resuelven completamente en los módulos de características que los utilizan, sin el uso de entidades adicionales como mediadores).
Esto fue seguido por una serie de preguntas, que pondré casi sin cambios aquí en el formato de "pregunta-respuesta":
- ¿Cómo se hace la autorización? ¿Cómo lo arrastra a los módulos de características?
- Las funciones con nosotros no dependen de la autorización, ya que casi todas las acciones de la aplicación ocurren en la zona autorizada.
- ¿Cómo rastrear y limpiar los componentes no utilizados?
- Tenemos una entidad como InjectorRefCount (implementada a través de WeakHashMap), que, al eliminar la última Actividad (o fragmento) que usa este componente, la elimina.
- ¿Cómo medir una exploración "limpia" y el tiempo de construcción? Si se encienden los cachés, se obtiene un escaneo bastante sucio.
- Puede deshabilitar Gradle Cache (org.gradle.caching en gradle.properties).
- ¿Cómo ejecutar pruebas unitarias desde todos los módulos en modo de depuración? Si ejecuta solo la prueba de gradle, se extraen las pruebas de todos los sabores y buildType.
(Esta pregunta provocó la discusión de muchos participantes en la reunión).
- Puedes intentar ejecutar testDebug.
- Entonces los módulos para los que no hay configuración de depuración no se ajustarán. Comienza demasiado o muy poco.
- Puede escribir una tarea de Gradle, que anulará testDebug para dichos módulos, o realizará una configuración de depuración falsa en el módulo build.gradle.
- Puede implementar este enfoque de esta manera:
withAndroidPlugin(project) { _, applicationExtension -> applicationExtension.testVariants.all { testVariant -> val testVariantSuffix = testVariant.testedVariant.name.capitalize() } } val task = project.tasks.register < SomeTask > ( "doSomeTask", SomeTask::class.java ) { task.dependsOn("${project.path}:taskName$testVariantSuffix") }

La siguiente presentación improvisada fue hecha por Evgeny Krivobokov y Mikhail Yudin de Avito.
Usaron mapas mentales para visualizar su historia.
Ahora el proyecto de la compañía tiene> 300 módulos, con el 97% de la base de código escrita en Kotlin. El objetivo principal del desglose en módulos era acelerar el ensamblaje del proyecto. El desglose en módulos se produjo gradualmente, y las partes menos dependientes del código se asignaron a los módulos. Para hacer esto, se desarrolló una herramienta para marcar las dependencias de los códigos fuente en el gráfico para el análisis de impacto ( informe sobre el análisis de impacto en Avito ).
Con esta herramienta, puede marcar un módulo de características como final para que otros módulos no puedan depender de él. Esta propiedad se verificará durante el análisis de impacto y proporciona una designación de dependencias explícitas y acuerdos con los equipos responsables del módulo. Basado en el gráfico construido, la distribución de cambios también se verifica para ejecutar pruebas unitarias para el código afectado.
La compañía utiliza un mono-repositorio, pero solo para fuentes de Android. El código de otras plataformas vive por separado.
Gradle se usa para construir el proyecto (aunque los colegas ya están pensando en un coleccionista como Buck o Bazel más adecuado para proyectos de módulos múltiples). Ya probaron Kotlin DSL, y luego volvieron a Groovy en los scripts de Gradle, porque es inconveniente admitir diferentes versiones de Kotlin en Gradle y en el proyecto: la lógica general se pone en complementos.
Gradle puede paralelizar tareas, caché y no recompilar dependencias binarias si su ABI no ha cambiado, lo que garantiza un ensamblaje más rápido de un proyecto de varios módulos. Para un almacenamiento en caché más eficiente, se utilizan Mainfraimer y varias soluciones autoescritas:
- Al cambiar de rama en rama, Git puede dejar carpetas vacías que rompen el almacenamiento en caché ( problema Gradle # 2463 ). Por lo tanto, se eliminan manualmente usando el gancho Git.
- Si no controla el entorno en las máquinas de los desarrolladores, las diferentes versiones del SDK de Android y otros parámetros pueden degradar el almacenamiento en caché. Durante la compilación del proyecto, el script compara los parámetros del entorno con los esperados: si se instalan las versiones o parámetros incorrectos, la compilación caerá.
- Analytics está activando / desactivando parámetros y el entorno. Esto es para monitorear y ayudar a los desarrolladores.
- Los errores de compilación también se envían a análisis. Los problemas conocidos y populares se ingresan en una página especial con una solución.
Todo esto ayuda a lograr un 15% de pérdida de caché en CI y 60-80% localmente.
Los siguientes consejos de Gradle también pueden ser útiles si aparece una gran cantidad de módulos en su proyecto:
- Deshabilitar módulos a través de indicadores IDE es inconveniente; estos indicadores se pueden restablecer. Por lo tanto, los módulos se deshabilitan a través de settings.gradle.
- En el estudio 3.3.1 hay una casilla de verificación "Omitir la generación de fuente en la sincronización de Gradle si un proyecto tiene más de 1 módulos". Por defecto, está apagado, es mejor encenderlo.
- Las dependencias se registran en buildSrc para reutilizarlas en todos los módulos. Otra opción es Plugins DSL , pero no puede colocar la aplicación del complemento en un archivo separado.
Nuestra reunión terminó con Vladimir de Tinkoff con el título clickbait del informe, "Cómo reducir la asamblea en relaciones públicas de 40 minutos a cuatro" . De hecho, estábamos hablando de la distribución de los inicios de gradle-plugs: compilaciones de apk, pruebas y analizadores estáticos.
Inicialmente, los chicos en cada solicitud de extracción realizaron un análisis estático, directamente el ensamblaje y las pruebas. Este proceso tomó 40 minutos, de los cuales solo Lint y SonarQube tomaron 25 y cayeron solo el 7% de los lanzamientos.
Por lo tanto, se decidió poner su lanzamiento en un trabajo separado, que se ejecuta en un horario cada dos horas y, en caso de error, envía un mensaje a Slack.
La situación opuesta estaba usando Detect. Se estrelló casi constantemente, razón por la cual se puso en una verificación preliminar previa.
Por lo tanto, solo el ensamblaje apk y las pruebas unitarias permanecieron en la verificación de solicitud de extracción. Las pruebas compilan las fuentes antes de ejecutarlas, pero no recopilan recursos. Dado que la fusión de recursos casi siempre tuvo éxito, el ensamblaje apk también se abandonó.
Como resultado, solo el lanzamiento de las pruebas unitarias permaneció en la solicitud de extracción, lo que nos permitió alcanzar los 4 minutos indicados. La compilación de apk se lleva a cabo con la solicitud de fusión en dev.
A pesar de que la reunión duró casi 4 horas, no logramos discutir el tema candente de organizar la navegación en un proyecto de varios módulos. Quizás este sea el tema de las próximas conversaciones móviles de Kaspersky. Además, a los participantes realmente les gustó el formato. Cuéntanos de qué te gustaría hablar en la encuesta o en los comentarios.
Y finalmente, enlaces útiles desde el mismo chat: