Avoid Gradle Duplication in Multi-Module Android

When you have an Android project with many modules, you may find a lot of the gradle configuration being duplicated (often by copy and paste) across the build.gradle files in the various modules.

I’ve read various ways to try to avoid this on the internet, and came across this blog post recently. This post uses the name of the module to determine which gradle plugins to apply and which configuration to use for that module.

In one of my multiple module projects, I have used something similar. However the difference is that I apply the plugins required in each individual module build.gradle, and in the root build.config I use both the module name and it’s properties to add the appropriate shared configuration.

For example, here is the common shared configuration for all modules in the root build.gradle:

subprojects {
    afterEvaluate { project ->
        if (project.hasProperty("android")) {
            android {
                compileSdkVersion 30
                buildToolsVersion '30.0.2'
                defaultConfig {
                    minSdkVersion 23
                    targetSdkVersion 30
                }
                compileOptions {
                    sourceCompatibility JavaVersion.VERSION_11
                    targetCompatibility JavaVersion.VERSION_11
                }
            }
        }
}

This app is a mixed Java and Kotlin project so only some of the modules use Kotlin. In the Kotlin modules, I apply the kotlin plugin.

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

Then in the root build.gradle, I can add the Kotlin configurations to those modules:

if (project.hasProperty('kotlin')) {
        android {
            kotlinOptions {
                jvmTarget = JavaVersion.VERSION_11.toString()
            }
        }
        dependencies {
            implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
            implementation 'androidx.core:core-ktx'
        }
}

If there is common configuration that is only applicable to some modules, I can also use the module name as a filter to add that configuration for only those modules. For instance for all modules excluding the app module:

if (project.hasProperty('android') && !name.equalsIgnoreCase('app')) {
      ...
}

Likewise for modules with particular names:

if (project.hasProperty('android') && name.equalsIgnoreCase('feature-1')) {
      ...
}

Note that I use the root build.gradle to contain the common configuration to keep things simple, but there are also more complicated methods around that use the buildSrc directory instead.

Gradle Dependencies for Java, use compile or implementation?

While I was explaining to a colleague about using Gradle for Java projects (he was moving away from Maven), we came across various code samples. Some of the examples were using the compile configuration for dependencies, while others were using implements and api.


dependencies {
compile 'commons-httpclient:commons-httpclient:3.1'
compile 'org.apache.commons:commons-lang3:3.5'
}


dependencies {
api 'commons-httpclient:commons-httpclient:3.1'
implementation 'org.apache.commons:commons-lang3:3.5'
}

This post was a summary based on the documentation and StackOverflow questions to explain to him which configurations to use.

New Dependency Configurations

Gradle 3.4 introduced the Java-library plugin, which included the then new configurations implementation and api (amongst others). These were meant to replace the compile configuration which was deprecated for this plugin. The idea was that the new configurations would help to prevent leaking of transitive dependencies for multi-module projects.

Please note that in this post I am just using the compile vs implementation/api configurations as an example. Other new replacement configurations were introduced as well, please read the documentation for further information.

Java

For a Java project using Gradle 3.4+, then it depends on whether you are build an application or a library.

For a library project or a library module in a multiple module project, it is recommended to use the Java-library plugin, so in build.gradle use this

apply plugin: 'java-library'

instead of

apply plugin: 'java'

Then you would use either implementation or api, depending on whether you want to expose the dependency to consumers of the library.

For a plain application project, you can stick with the java plugin and continue to use the compile configuration. Having said that, I have tried using the Java-library plugin for an app project and it seems to work fine.

Android

For an Android project, the new configurations came with the Android Gradle Plugin 3.0. So unless you are still using the 2.x version of Android Studio / Android Gradle plugin, the use of compile is deprecated. So you should use implementation, even for an app.

In fact, when I recently upgraded my Android Studio, it came up with the message:

Configuration ‘compile’ is obsolete and has been replaced with ‘implementation’.
It will be removed at the end of 2018

I think this also applies if you use Kotlin instead of Java.

Groovy

How about a project with Groovy as well as Java? This can be for a mixed Groovy / Java project, or for a Java project which needs Groovy for some support tools (such as Spock or Logback configuration).

In the past I have used the Groovy plugin instead of the Java plugin for mixed projects. The Groovy plugin extends the Java plugin and will handle the compilation for Java sources as well as Groovy sources.

apply plugin: 'groovy'

You can continue to do this for Java application modules, but the documentation states that the Groovy plugin has compatibility issues with the Java-library plugin so will need a work around for library modules.

Of course this short post is for newbies, and has only scratched the surface in terms of learning about all the new dependency configurations.