Muchas veces durante el desarrollo de aplicaciones nos encontramos que tenemos que resolver el mismo problema varias veces. La solución a esto es extraer la solución a una librería para poder ser reutilizada en otros proyectos.

Cuando hacemos esto en un proyecto Android nos encontramos con una elección, crear una librería para Android o crear una librería Java/Kotlin.

Si nuestra solución depende del framework de Android, recursos gráficos o librerías de AndroidX, escogeremos hacer una librería Android.

Si, por otro lado, no dependemos de ninguna de estas cosas, podemos generar una librería para toda la JVM, que podremos reusar en otros proyectos, como aplicaciones de escritorio o incluso en backend.

Publicar librerías Android multi módulo

¿Pero qué hacemos si nuestra solución puede ser implementada sin dependencias al ecosistema de Google pero tenerlas facilitaría la implementación en Android?

Tenemos varias soluciones a este problema:

  • Si no le vemos posibilidades de uso fuera de Android podemos mantener una sola versión de la librería.
  • Podemos crear dos proyectos de librería distintas: una para la JVM y otra para Android. Pero esto nos obligará a duplicar mucho código.
  • Podemos tener dos proyectos de librería pero esta vez hacer que la de Android dependa de la versión para la JVM. De esta manera esta librería tendrá solo el código que facilita la implementación en mobile sin duplicar lo que hay en la librería de Java/Kotlin. El problema de esto es que nos obligará a tener dos repositorios distintos para código que está muy fuertemente relacionado: cambios en la API de la JVM probablemente afectará a la implementación y API de la versión android.
  • Tener un solo proyecto multi módulo y publicar cada módulo por separado. De este modo tenemos todos los beneficios del punto anterior pero además podemos, por ejemplo, usar las herramientas de refactor del AndroidStudio para realizar cambios en la API sin romper ninguna de las librerías.

En este artículo nos vamos a centrar en el último punto, publicar librerías Android multi módulo. Especialmente en cómo configurar gradle para poder publicar nuestros módulos de forma cómoda y sin tener que duplicar la lógica de publicación.

La estructura de módulos del proyecto será la siguiente:

  • sample: aplicación Android que muestra cómo usar y testear nuestra librería
  • core: librería Kotlin/JVM con el core de nuestra librería con toda la funcionalidad posible sin dependencias al framework de Android
  • android: librería Kotlin/Android con las extensiones o APIs disponibles solo en Android
  • test: API de test que faciliten la escritura de tests cuando nuestra librería sea usada

Nuestro objetivo es llegar a publicar nuestros distintos módulos como: “com.group:mylibrary-module:1.0.0”

Con tal de tener un sistema de nombres consistente en todas las librerías publicadas en el archivo ‘settings.gradle’ añadiremos esta línea arriba del todo:

rootProject.name = "mylibrary"

La configuración de los distintos módulos es la habitual para cada uno de ellos. Hay que tener en cuenta que cuando se desarrolla una API de test las dependencias de Junit pasan de ser testImplementation a ser implementation pues la implementación del código puede necesitar aserciones, por ejemplo.

Con tal de no duplicar la configuración de publicación en cada uno de los módulos esta configuración la vamos a hacer en el ‘build.gradle’ del proyecto.

Para una primera configuración y con tal de poder probar que lo que estamos haciendo funciona primero vamos a publicar las librerías en el repositorio local de maven de nuestra máquina.

Para ello vamos a añadir el siguiente plugin a nuestro script de gradle:

plugins {
   id "maven-publish"
}

Este plugin nos facilitara toda la gestión del publicado de las librerías así como la configuración. En la sección de ‘allprojects’ podemos añadir el grupo y la versión de las librerías. Idealmente, ya que tanto el módulo de test como el de android depende del core se deberían actualizar todas las versiones a la vez. Esto además evita confusiones para los usuarios de la librería pues un solo número de versión es suficiente.

allprojects {
   group = "com.group"
   version = "1.0.0"
   repositories {
       google()
       jcenter()
   }
}

Ahora creamos un nuevo bloque en el script para configurar la publicación de los módulos. Como no queremos publicar el proyecto entero sino cada uno de los módulos usaremos el bloque ‘subprojects’ para ello. La primera línea del bloque será:

apply plugin: "maven-publish"

Esto aplicará el plugin que hemos añadido al proyecto en cada uno de los submódulos sin tener que editar sus ficheros uno a uno.

A continuación añadimos la configuración básica:

publishing {
   publications {
       maven(MavenPublication) {
           artifactId = "$rootProject.name-$project.name"
       }
   }
}

Si ahora ejecutamos la tarea de gradle: ‘publishToMavenLocal’ podremos ver en el repositorio local (normalmente situado en “$USER/.m2/repository”) que se ha añadido en com/group/ cuatro directorios: mylibrary-core, mylibrary-test, mylibrary-android, mylibrary-sample.

Aquí podemos ver un primer problema, nosotros no queremos publicar la aplicación de muestra esta está en el repositorio para servir de ejemplo o realizar pruebas y no debería ser publicada. Por otro lado si entramos dentro podemos ver dos problemas más: Los archivos .pom no incluyen las dependencias de los módulos y no se están incluyendo los .jar ni los .aar.

Para solucionar estos problemas tendremos que modificar el bloque de publishing para tener estas cosas en cuenta.

Primero lo primero, vamos a evitar que las aplicaciones android que se incluyan en el proyecto sean publicadas. Esto ocurre porque la tarea ‘publishToMavenLocal’ busca todos los módulos que definen un bloque de publishing y lo publican. Como no queremos duplicar la configuración en todos los módulos tenemos que evitar de algún modo que ese módulo en concreto se publique.

Para ello vamos a necesitar alguna forma de identificar al módulo, el nombre sería una primera opción pues sabemos como se llama el modulo, pero esto obligaría a cambiar la configuración si algún día le cambiamos el nombre o añadimos otro ejemplo distinto. Pero realmente lo queremos es excluir las aplicaciones Android y dejar los módulos de librerías, lo que haremos será identificar qué módulos son aplicaciones y para ellos no definiremos el bloque de ‘publishing’. Para saber si un módulo es una aplicación solo hay que comprobar si ha aplicado el plugin: com.android.application en su script de gradle:

afterEvaluate {
   if (!plugins.hasPlugin("android")) {
       publishing {
           publications {
               maven(MavenPublication) {
                   artifactId = "$rootProject.name-$project.name"
               }
           }
       }
   }
}

Necesitamos el bloque ‘afterEvaluate’ pues queremos que el proyecto ya tenga los plugins aplicados para poder comprobar si es una aplicación.

Si borramos lo generado anteriormente y volvemos a publicar podemos comprobar como esta vez mylibrary-sample no ha sido publicado.

Para arreglar el problema de que no se estén generando los .jar ni las dependencias en los .pom podemos incluir la linea from components.java dentro de la publicación:

maven(MavenPublication) {
       artifactId = "$rootProject.name-$project.name"
       from components.java
   }

Pero si intentamos sincronizar los ficheros de gradle veremos que genera un error. Este error es debido a que no todos nuestros módulos incluyen el plugin de java necesario para poder usar los componentes. De nuevo podemos añadir un condicional y usar el componente solo donde el plugin esté definido:

if (plugins.hasPlugin("java")) {
   from components.java
}

Si ahora sincronizamos y publicamos en local de nuevo veremos que mylibrary-core y mylibrary-test incluyen ahora los .jar respectivos así como sus dependencias en el .pom.

Pero mylibrary-android sigue sin tener ninguna de las cosas. El problema es que mylibrary-android es una librería android y no una librería de la jvm normal y corriente. Y por tanto tendríamos que configurar a mano como exportar las dependencias al .pom, como generar un .aar, etc. Por suerte este es un problema recurrente y existe una solución para ello.

Para la publicación de la librería android vamos a usar el plugin android-maven-publish de digital.wup. Para ello lo añadimos al script:

plugins {
   id "maven-publish"
   id "digital.wup.android-maven-publish" version "3.6.2"
}

Luego cambiamos la configuración de submódulos y de publicación para usarlo:

subprojects {
   apply plugin: "maven-publish"
   apply plugin: "digital.wup.android-maven-publish"

  afterEvaluate {
       if (!plugins.hasPlugin("android")) {
           publishing {
               publications {
                   maven(MavenPublication) {
                       artifactId = "$rootProject.name-$project.name"
                       if (plugins.hasPlugin("java")) {
                           from components.java
                       } else if (plugins.hasPlugin("android-library")) {
                           from components.android
                       }
                   }
               }
           }
       }
   }
}

De nuevo, tenemos que añadir un condicional que compruebe que el módulo tiene aplicado el plugin “android-library” para poder usar el components.android. Ahora si, si sincronizamos y publicamos podemos ver que mylibrary-android contiene el .aar así como sus dependencias en el pom.

Ahora que ya hemos verificado que la publicación funciona como queremos podemos añadir nuestros repositorio remoto. Por ejemplo podemos añadir un repositorio de Snapshots con tal de que nuestro equipo de desarrollo la pueda empezar a usar sin tener que esperar a releases completas y así nos ayuden a encontrar errores antes de sacar versiones públicas. Para ello añadimos en el bloque publishing debajo de publications nuestra configuración:

repositories {
   maven {
       name 'NexusSNAPSHOT'
       url snapshotsRepositoryUrl
       credentials {
           username = deployRepoUsername
           password = deployRepoPassword
       }
   }
}

Es recomendable que tanto el usuario como la contraseña no se suban al repositorio y se guarden el fichero local de local.properties. Ahora si ejecutamos la tarea de gradle:

publishMavenPublicationToNexusSNAPSHOTRepository

Nuestros módulos se subirán como dependencias separadas al repositorio y los usuarios podrán usar solo las partes que necesiten en sus proyectos.

Hay que tener en cuenta si queremos añadir nuevos artefactos, como el código fuente o la documentación, que las tareas para generarlas se deben definir en dentro del bloque de subprojects y que probablemente algunos de estos artefactos tengan dependencias con los plugins de java o android.library. Por ejemplo, así podríamos generar los .jar con los sources:

if (plugins.hasPlugin("java")) {
   task jvmSourcesJar(type: Jar) {
       archiveClassifier.set("sources")
       from sourceSets.main.allSource
   }
}
if (plugins.hasPlugin("android-library")) {
   task androidSourcesJar(type: Jar) {
       archiveClassifier.set("sources")
       from android.sourceSets.main.java.srcDirs
   }
}