Publishing Multi-Module Android Libraries
A lot of times, while developing mobile applications we realize that we have to solve the same problem time and time again. The solution to this issue is to extract the solution from a library so it can be re-used in other projects.
Join the DZone community and get the full member experience.
Join For FreeA lot of times, while developing mobile applications, we realize that we have to solve the same problem time and time again. The solution to this issue is to extract the solution from a library so it can be re-used in other projects.
When we do this in an Android project, we’re faced with a choice: creating an Android library or a Java/Kotlin one.
If our solution depends on an Android framework, graphic resources or AndroidX libraries, then we’ll choose Android. If, on the other hand, if we don’t depend on any of these things, we can create a library for all the JVM that we’ll be able to re-use on other projects, like desktop applications or even backends.
Publishing Multi-Module Android Libraries
But, what do we do if our solution can be implemented without dependencies to Google’s ecosystem, but having them would ease its implementation in Android?
We can find various solutions to this issue:
- If we don’t see any chance of it being used outside of Android, we can keep a single version of the library.
- We can create two different library projects: one for the JVM and the other for Android. This would force us to duplicate a lot of code, though.
- We can have two library projects but, this time, make the Android library depend on the JVM one. This way, the library will only have the code that facilitates its mobile implementation without duplicating what’s on the Java/Kotlin library. The problem with this approach is that it’ll force us to create two different repositories for code that’s strongly related: changes to the JVM library’s API will probably affect Android’s implementation and API.
- Having a single, multi-module project and publishing each module separately. This way, we have all the benefits of the last option plus now we also can, for example, use AndroidStudio’s refactor tools to make some changes to the API without breaking any of the libraries.
In this article, we’ll focus on the last point, publishing multi-module Android libraries — especially on how to set Gradle up so we can publish our own modules in a comfortable way, without the need to duplicate the publication’s logic.
The project’s module structure will be as follows:
- sample: Android application that shows how to use and test our library
- core: Kotlin/JVM core with our library’s core, with all the possible functionality without dependencies to Android’s framework
- android: Kotlin/Android library with the extensions or APIs available only in Android
- test: test API that facilitates test writing when our library is in use
Our goal is to publish our many modules as: “com.group:mylibrary-module:1.0.0”
So we can have a consistent naming system with all published libraries in the archive ‘settings.gradle’. we’ll add this line on the top:
xxxxxxxxxx
rootProject.name = "mylibrary"
The setup of the different modules is the usual for each of them. We need to take into account that when we develop a test API, JUnit’s dependencies go from testImplementation to implementation, since the code’s implementation might need assertions, for example.
We don’t want to duplicate the publication setup in each of the modules, so we’ll do it in the project’s ‘build.gradle’.
For a first configuration and so we can check that what we’re doing works, first we’ll publish the libraries on the local maven repository of our machine. To that end, we’ll add the next plugin to our gradle script:
xxxxxxxxxx
plugins {
id "maven-publish"
}
This plugin will facilitate the management of the libraries’ publication and configuration. In the ‘allprojects’ section, we can add the libraries’ group and version. Ideally, all versions should be updated at the same time, since both the test and android module depend on the core. This also avoids confusion among library users since a single version number is enough.
xxxxxxxxxx
allprojects {
group = "com.group"
version = "1.0.0"
repositories {
google()
jcenter()
}
}
Now, we’ll create a new block in the script to setup the modules’ publication. Since we don’t want to publish the entire project but each of the modules, we’ll use the ‘subprojects’ block. The block’s first line will be:
xxxxxxxxxx
apply plugin: "maven-publish"
This will apply the plugin that we’ve added to the project in every submodule without the need to individually edit its files one by one.
Next, we’ll add the basic setup:
xxxxxxxxxx
publishing {
publications {
maven(MavenPublication) {
artifactId = "$rootProject.name-$project.name"
}
}
}
If we now run the Gradle task, ‘publishToMavenLocal’, we’ll see that in the local repository (usually located at “$USER/.m2/repository”) 4 new directories have been added to com/group/: mylibrary-core, mylibrary-test, mylibrary-android, mylibrary-sample.
We can see the first problem here. We don’t want to publish our test application — it’s in the repository as an example or to run tests, and it mustn’t be published. If we enter the repository, we’ll see two extra problems: the .pom files don’t include the modules’ dependencies, and neither the .jar nor the .aar files are being included.
To solve these issues, we’ll have to modify the publishing block to take these into account.
First things first, let’s avoid the publication of the Android applications included in the project. This happens because the ‘publishToMavenLocal’ task searches all the modules that define a publishing block and publishes it. Since we don’t want to duplicate the configuration in all the modules, we’ll have to prevent this particular module from being published.
To that end, we’ll need some way to identify the module. The name would be an option, since we know what the module’s called, but this would force us to change the configuration once/if we change its name or add a different example.
But since what we really want to do is to exclude the Android applications and leave the library modules, what we’ll do is identify which modules are applications, and to that end, we’ll define the ‘publishing’ block. To know if a module is an application, we’ll only need to check if it applies the com.android.application plugin in its gradle script:
xxxxxxxxxx
afterEvaluate {
if (!plugins.hasPlugin("android")) {
publishing {
publications {
maven(MavenPublication) {
artifactId = "$rootProject.name-$project.name"
}
}
}
}
}
We need the ‘afterEvaluate’ block since we want the plugins already applied to the project so we can check if it’s an application.
If we delete what was previously generated and we publish again, we can check how this time, mylibrary-sample hasn’t been published.
To solve the issue of the .jar files not being generated, as well as the .pom files’ dependencies, we can add the ‘from components.java’ line in the publication.
xxxxxxxxxx
maven(MavenPublication) {
artifactId = "$rootProject.name-$project.name"
from components.java
}
But, if we try to synchronize the Gradle files, we’ll see it generates an error. This error happens because not all of our modules include the Java component needed. We can, once again, add a conditional and use the component only when/if the plugin is defined:
xxxxxxxxxx
if (plugins.hasPlugin("java")) {
from components.java
}
If we synchronize now and locally publish, we’ll see that mylibrary-core and mylibrary-test now include all the respective .jar as well as their .pom dependencies, but mylibrary-android still lacks both things.
The issue here is that mylibrary-android is an Android library, and not a normal JVM library, so we’d need to manually set up how to export the .pom dependencies, how to generate an .aar file, etc… Thankfully, this is a common issue, and there’s a solution available.
For the publication of the Android library, we’ll use digital.wup’s plugin: android-maven-publish plugin. We’ll add it to the script:
xxxxxxxxxx
plugins {
id "maven-publish"
id "digital.wup.android-maven-publish" version "3.6.2"
}
Then, we change the submodule and publication configuration to use it:
xxxxxxxxxx
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
}
}
}
}
}
}
}
Once again, we’ll add a conditional that checks if the module has the “android-library” plugin applied so it can use components.android. And now, finally, if we synchronize and publish, we can see that mylibrary-android contains the .aar file as well as the .pom dependencies.
Now that we’ve verified that the publication works as we want it to, we can add our remote repositories. For example, we can add a Snapshots repository so our development team can begin to use it without the need to wait for complete releases and so they can help us find errors before releasing public versions. To that end, we’ll add our configuration in the publishing block, below the publications:
xxxxxxxxxx
repositories {
maven {
name 'NexusSNAPSHOT'
url snapshotsRepositoryUrl
credentials {
username = deployRepoUsername
password = deployRepoPassword
}
}
}
It is recommended that we don’t upload neither the user nor the password to the repository, and instead we save them in the local.properties file. Now if we add the Gradle task:
publishMavenPublicationToNexusSNAPSHOTRepository
Our modules will be uploaded to the repository as separated dependencies and the users will be able to use only the parts they need in their projects.
documentation, that the tasks to generate them must be defined inside the subprojects block, and that some of these artifacts might probably have dependencies with the java plugins or android.library. For example, this is how we can generate the .jar with the sources:
xxxxxxxxxx
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
}
}
Published at DZone with permission of Eric Martori. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments