Android Maven Publish For Your Libraries
Tags: Maven Plugin, Android App Development, Maven, Android, AndroidDev

This article will save your day in attempts of doing preparation & uploads of your Android libraries’ AAR artifacts to Maven.

Photo by Jen Teodore on Unsplash

Introduction

I’m pretty sure, if you are reading this article you’re interested in the topic of publishing artifacts to Maven😊 At least, that was true for me some months ago when I was searching for any tip, any working script, or any answer to help me with making artifacts for the API I was developing. Honestly speaking, making that working in all cases that were relevant for me was pretty challenging, as it was very new to me. If you see this is somehow connected to your situation, let’s continue and go step by step to the working script & uploads.

Using Maven Publish Plugin

Maven Publish plugin is one of the ways to prepare and upload your library or application artifacts to Maven repository. In many projects, Maven repository is used to share libraries between developers, especially when it comes to some shared frameworks. Maven Publish is the plugin recommended by the Google Android community. How it’s described in the official docs:

The Maven Publish Plugin provides the ability to publish build artifacts to an Apache Maven repository. A module published to a Maven repository can be consumed by Maven, Gradle (see Declaring Dependencies), and other tools that understand the Maven repository format. You can learn about the fundamentals of publishing in Publishing Overview.

To start using Maven Publish plugin in your Android project simply add this code to the module’s build.gradle which you’re going to publish:

plugins {

id 'maven-publish'

}

or in the module’s build.gradle.kts:

plugins {

`maven-publish`

}

As mentioned in Android guide for using this plugin, you can do not only library publication artifacts, using this script, but also application artifacts.

The Android Gradle plugin creates a component for each build variant artifact in your app or library module that you can use to customize a publication to a Maven repository. The components that the Android plugin creates depend on whether the module uses the application or library plugin, as described in the table below.

Once you sync the project you’ll have access to Gradle tasks for publishing:

Same tasks can be run in Console with ./gradlew. Most probably you’ll be using publishToMavenLocal or publish for most of your cases.

In the current article, we’ll consider the preparation & upload of AAR library publication artifacts for the multi modular Android library.

Project Specifics

Before getting to the script itself we should be on the same page and understand the specifics of the library, its’ structure and versioning. The current project is a framework, consisting of 10+ modules. This framework is used as one AAR dependency in other Android projects, as we don’t support the separation of a framework into parts.

Generally, this is a very good idea to design your framework as separate libraries so that developers can choose which functions to add to their projects without adding redundant code. But in some cases framework should be represented as one AAR dependency, as its submodules are not atomic and don’t bring much value when used separately. Anyway, this is not a topic of the current article, please design your libraries & framework according to your specific needs 😊

Therefore, if you have many modules that are part of your single AAR dependency it’s very convenient to have no versioning support in each module. Hence, developers don’t have to support versions for submodules when they’re doing some code modifications. So if a developer, for instance, has changed caching logic in the caching module of the framework it’s a new version of framework generally, and only global pomVersion should be updated. Practically it’s 2 constants in the project build.gradle:

ext {

pomVersion = '1.0.0'

pomGroupID = "test.framework.com"

}

For the current example we should simplify our project structure to have 4 modules:

- base

- caching (depends on base)

- ui (depends on base)

- framework (includes base, caching, ui)

If you simply run publish task in such a situation, you’ll receive a POM file & AAR artifact, but once you try to use it from elsewhere — you’ll see Gradle sync issues with not found ‘unspecified’ dependencies for submodules.

Example:

<?xml version=”1.0" encoding=”UTF-8"?>

<project xmlns=”http://maven.apache.org/POM/4.0.0" xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<groupId>test.framework.com</groupId>

<artifactId>framework</artifactId>

<version>1.0.0</version>

<dependencies>

<dependency>

<artifactId>base</artifactId>

<groupId>test.framework.com</groupId>

<version>unspecified</version>

</dependency>

<dependency>

<artifactId>caching</artifactId>

<groupId>test.framework.com</groupId>

<version>unspecified</version>

</dependency>

<dependency>

<artifactId>ui</artifactId>

<groupId>test.framework.com</groupId>

<version>unspecified</version>

</dependency>

</dependencies>

</project>

That means we need to somehow customise POM file generation, so that instead of unspecified we can get real versions at least.

Customizing POM File Generation — Basics

First of all, let’s start with the version setup, so it’s taken from project build.gradle:

publishing {

publications {

maven(MavenPublication) {

groupId project.ext.pomGroupID

artifactId project.name

version project.ext.pomVersion



artifact(bundleReleaseAar)

}

}

}

Now our modules receive the common version from pomVersion and groupId from project pomGroupId. To proceed, we need to also work with the POM file generation itself. The POM file is an XML file with nodes, as you might have mentioned from above sample, and the thing which cases most of troubles is ‘dependencies’ node. Our goal is to resolve ‘unspecified’ versions first, and this should be possible via this part:

pom.withXml {

final dependenciesNode = asNode().appendNode('dependencies')

ext.addDependency = { Dependency dep, String scope ->

if (dep.group == null || dep.version == null || dep.name == null || dep.name == "unspecified")

return // invalid dependencies should be ignored



final dependencyNode = dependenciesNode.appendNode('dependency')

dependencyNode.appendNode('artifactId', dep.name)

if (dep.version == 'unspecified') {

dependencyNode.appendNode('groupId', project.ext.pomGroupID)

dependencyNode.appendNode('version', project.ext.pomVersion)

} else {

dependencyNode.appendNode('groupId', dep.group)

dependencyNode.appendNode('version', dep.version)

}

...

}

}

This part is done in the assumption that ‘unspecified’ versions in our project example should only come from submodules that don’t have defined versions in place. As you can see in this sample we’re writing nodes of dependencies by ourselves, and if version is ‘unspecified’ it’s replaced with the project version.

Moreover, when we do POM file generation ourselves, we should take care of the source of dependencies as well. So after declaring addDependency function we should add calls to pass dependencies to this function:

...

configurations.api.getDependencies().each { dep -> addDependency(dep, "compile") }

configurations.implementation.getDependencies().each { dep -> addDependency(dep, "runtime") }

...

In this case, dependencies preserve the origin of their declaration from build.gradle so that:

“ api ” transforms to “ compile ”, meaning dependency is available before runtime, you can use it during developing code in the dependant project

” transforms to “ ”, meaning dependency is available before runtime, you can use it during developing code in the dependant project “implementation” transforms into “runtime”, meaning dependency is not available during development but is used in the runtime.

Having these lines we’re not breaking the availability of included libraries and submodules, and this is extremely important for the Framework development. To make “scope” also part of the dependency node we should add this line into addDependency function after the code for setting version:

dependencyNode.appendNode('scope', scope)

Now we’re good with the basic POM file generation customization and Gradle sync task doesn’t cause issues in many cases. Besides some things are still uncovered, like transitive dependencies, exclusions, and dependency type. This is a very important part, that required the most efforts in this customization, so if you’re interested — let’s dive into it!

Finalising POM File Customisation — Advanced

To complete our customization we should first take care of the transitive dependencies, as you may have some in your project. If you’re not yet familiar with the transitive dependencies, please have a look into Gradle docs.

Handling of transitive dependencies come together with the exclusion groups being taken into account. As a result we should update the script with this piece of code:

...

if (!dep.transitive) {

// In case of non transitive dependency, all its dependencies should be force excluded from them POM file

final exclusionNode = dependencyNode.appendNode('exclusions').appendNode('exclusion')

exclusionNode.appendNode('groupId', '*')

exclusionNode.appendNode('artifactId', '*')

} else if (!dep.properties.excludeRules.empty) {

// For transitive with exclusions, all exclude rules should be added to the POM file

final exclusions = dependencyNode.appendNode('exclusions')

dep.properties.excludeRules.each { ExcludeRule rule ->

final exclusionNode = exclusions.appendNode('exclusion')

exclusionNode.appendNode('groupId', rule.group ?: '*')

exclusionNode.appendNode('artifactId', rule.module ?: '*')

}

}

After all is set the only thing we’re missing right now is type of the dependency. At times our module may depend on some JARs, but if it’s not by default Gradle will look for AAR artifacts, which may be unavailable. To handle this we should add the following script part after “scope” line:

// Some dependencies may have types, such as aar, that should be mentioned in the POM file

def artifactsList = dep.properties['artifacts']

if (artifactsList != null && artifactsList.size() > 0) {

final artifact = artifactsList[0]

dependencyNode.appendNode('type', artifact.getType())

}

Now our POM file is ready, woohoo! The result is looking much better and corresponds to the build.gradle dependencies declaration.

Photo by Anton Shuvalov on Unsplash

Now you’re all set for the local maven publishing process, so once you run publishToLocalMaven task — your dependencies folders with versions should be added to the local user ./m2 folder (for Mac users). But if you need to put your artifacts later to some remote Maven — you should continue reading.

Publishing To Remote Maven — URL, Credentials

Once your ‘publications’ part is finished we should set up the remote Maven configuration in our script:

repositories {

maven {

url 'your_maven_url'

credentials {

username your_user_name

password your_password

}

}

}

In our case Maven has some authentication and requires sending user_name and password. To adjust this section to your needs please refer to the Gradle official docs about Repositories declaration. As mentioned there, you can also configure different URLs for publishing release and snapshot versions, if this is supported by your app:

publishing {

repositories {

maven {

def releasesRepoUrl = "$buildDir/repos/releases"

def snapshotsRepoUrl = "$buildDir/repos/snapshots"

url = project.hasProperty('release') ? releasesRepoUrl : snapshotsRepoUrl

}

}

}

Great, so finally both local and remote Maven publishing is available to us and we can prepare & share with other developers our AAR artifacts.

Bonus: For your convenience I’ve added two Gradle tasks as they speed up publishing process:

task cleanBuildPublishLocal(type: GradleBuild) {

tasks = ['clean', 'build', 'publishToMavenLocal']

}



task cleanBuildPublish(type: GradleBuild) {

tasks = ['clean', 'build', 'publish']

}

The full version of the script is available here.