Hola, me llamo Andrey y estoy trabajando en las aplicaciones Tinkoff y Tinkoff Junior para la plataforma Android. Quiero hablar sobre cómo recopilamos dos aplicaciones similares de una base de código.
— , ̆ 14 . , (, ), , , (, ).
. 

Al comienzo del proyecto, consideramos varias opciones para su implementación y tomamos una serie de decisiones. Inmediatamente se hizo evidente que las dos aplicaciones (Tinkoff y Tinkoff Junior) tendrían una porción significativa del código común. No queríamos bifurcar la aplicación anterior y luego copiar las correcciones de errores y la nueva funcionalidad común. Para trabajar con dos aplicaciones a la vez, consideramos tres opciones: Gradle Flavors, Git Submodules, Gradle Modules.
Sabores de gradle
Muchos de nuestros desarrolladores ya han intentado usar sabores, además podríamos usar sabores multidimensionales para usar con sabores existentes.
Sin embargo, los sabores tienen un defecto fatal. Android Studio considera que el código solo es el código del sabor activo, es decir, lo que se encuentra en la carpeta principal y en la carpeta del sabor. El resto del código se considera texto junto con comentarios. Esto impone restricciones en algunas herramientas de estudio: búsqueda de uso de código, refactorización y otras.
Submódulos de Git
Otra opción para implementar nuestra idea es usar los submódulos de githa: transferir el código común a un repositorio separado y conectarlo como un submódulo a dos repositorios con el código para una aplicación específica.
Este enfoque aumenta la complejidad de trabajar con el código fuente del proyecto. Además, los desarrolladores aún tendrían que trabajar con los tres repositorios para realizar ediciones al cambiar la API del módulo común.
Arquitectura multimódulo
La última opción es cambiar a la arquitectura de múltiples módulos. Este enfoque está libre de las desventajas que tienen los otros dos. Sin embargo, la transición a una arquitectura de módulos múltiples requiere una refactorización que requiere mucho tiempo.
Cuando comenzamos a trabajar en Tinkoff Junior, teníamos dos módulos: un pequeño módulo API que describe cómo trabajar con el servidor y un gran módulo de aplicación monolítico, en el que se concentraba la mayor parte del código del proyecto.


Como resultado, queríamos obtener dos módulos de aplicación: adulto y junior y algún módulo básico común. Hemos identificado dos opciones:
- Poner código común en el módulo común común. Este enfoque es "más correcto", pero lleva más tiempo. Estimamos los volúmenes de reutilización de código en aproximadamente el 80%.

- Convierta el módulo de aplicación en una biblioteca y conecte esta biblioteca a los módulos para adultos y jóvenes delgados. Esta opción es más rápida, pero traerá código a Tinkoff Junior que nunca se ejecutará.

Teníamos tiempo en reserva y decidimos comenzar el desarrollo de acuerdo con la primera opción (el módulo común ) con la condición de cambiar a la opción rápida cuando se nos acaba el tiempo para la refactorización.
Al final, esto sucedió: transferimos parte del proyecto al módulo común y luego convertimos el módulo de aplicación restante en una biblioteca. Como resultado, ahora tenemos la siguiente estructura de proyecto:

Tenemos módulos con funciones que nos permiten distinguir entre un código "adulto", general o "infantil". Sin embargo, el módulo de aplicación todavía es lo suficientemente grande, y ahora aproximadamente la mitad del proyecto está almacenado allí.
Convertir la aplicación en una biblioteca
La documentación tiene instrucciones simples para convertir una aplicación en una biblioteca. Contiene cuatro puntos simples y, al parecer, no debería haber dificultades:
- Abra el archivo
build.gradle
módulo - Eliminar
applicationId
de la configuración del módulo - Al comienzo del archivo, reemplace el
apply plugin: 'com.android.application'
con el apply plugin: 'com.android.library'
- Guarde los cambios y sincronice el proyecto en Android Studio ( Archivo> Sincronizar proyecto con archivos Gradle )
Sin embargo, la conversión tomó varios días y la diferencia resultante resultó así:
- 183 archivos cambiados
- 1601 inserciones (+)
- 1920 eliminaciones (-)
¿Qué salió mal?
En primer lugar, en las bibliotecas, los identificadores de recursos no son constantes . En las bibliotecas, como en las aplicaciones, se genera un archivo R.java con una lista de identificadores de recursos. Y en las bibliotecas, los valores del identificador no son constantes. Java no le permite activar valores no constantes, y todos los modificadores deben reemplazarse con if-else.
Luego, nos encontramos con una colisión de paquetes.
Suponga que tiene una biblioteca que tiene package = com.example , y la aplicación con package = com.example.app depende de esta biblioteca. Luego, la clase com.example.R se generará en la biblioteca y com.example.app.R , respectivamente , en la aplicación. Ahora creemos la actividad com.example.MainActivity en la aplicación, en la que intentaremos acceder a la clase R. Sin una importación explícita, se utilizará la clase R de la biblioteca, en la que los recursos de la aplicación no se especifican, sino solo los recursos de la biblioteca. Sin embargo, Android Studio no resalta el error, y cuando intente cambiar de código a un recurso, todo estará bien.
Daga
Usamos Dagger como marco para la inyección de dependencias.
En cada módulo que contiene actividad, fragmentos y servicios, tenemos las interfaces habituales que describen los métodos de inyección para estas entidades. En los módulos de aplicación ( adultos y junor ), las interfaces del componente de daga heredan de estas interfaces. En los módulos, llevamos los componentes a las interfaces necesarias para este módulo.
Multibindings
El desarrollo de nuestro proyecto se simplifica enormemente mediante el uso de enlaces múltiples.
En uno de los módulos comunes, definimos una interfaz. En cada módulo de aplicación ( adulto , junior ) describimos la implementación de esta interfaz. Usando la anotación @Binds
, le @Binds
la daga que, en lugar de una interfaz, es necesario inyectar su implementación específica para una aplicación infantil o adulta. También a menudo recopilamos una colección de implementaciones de interfaz (Set o Map), y dichas implementaciones se describen en diferentes módulos de aplicación.
Sabores
Para diferentes propósitos, recopilamos varias opciones de aplicación. Los sabores descritos en el módulo base también deben describirse en los módulos dependientes. Además, para que Android Studio funcione correctamente, es necesario que se seleccionen opciones de ensamblaje compatibles en todos los módulos del proyecto.
Conclusiones
En poco tiempo hemos implementado una nueva aplicación. Ahora enviamos la nueva funcionalidad en dos aplicaciones, escribiéndola una vez.
Al mismo tiempo, pasamos algún tiempo refactorizando, reduciendo simultáneamente la deuda técnica, y pasamos a una arquitectura de módulos múltiples. En el camino, encontramos restricciones del SDK de Android y Android Studio, que gestionamos con éxito.