Easy modularity: Keeping your Gradle build scripts clean and eliminating duplication in your multi-module projects
Tags: gradle, android, java, kotlin
Published: 2020-08-10 17:51:58

---
title: Easy modularity: Keeping your Gradle build scripts clean and eliminating duplication in your multi-module projects
published: true
description: Using precompiled script plugins to simplify build logic
tags: gradle, android, java, kotlin
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/3f7l63gahha8t6ux06s9.jpg
---

I recently started breaking up the monolith I inherited at my current place of work. To make it easier for myself and for the rest of the team that will have to maintain it, I created three convention plugins in `buildSrc`. Gradle calls these [precompiled script plugins](https://docs.gradle.org/current/userguide/custom_plugins.html#sec:precompiled_plugins), and they can be written in either Groovy or Kotlin. The examples below are in Kotlin. Let's take a look!

# Enabling precompiled script plugins

**buildSrc/build.gradle.kts**
```kotlin
plugins {
  `kotlin-dsl`
}

repositories {
  jcenter()
  google()
}

dependencies  {
  implementation("com.android.tools.build:gradle:4.0.1")
  implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72")
}
```

This is an expansion upon the bare minimum that you will need if you have convention plugins that apply any of the Android or Kotlin plugins.

# The `android-library-convention` plugin

**buildSrc/src/main/kotlin/android-library-convention.gradle.kts**
```kotlin
plugins {
  id("com.android.library")
  id("kotlin-android")
}
android {
  compileSdkVersion(30)
  defaultConfig {
    minSdkVersion(21)
    targetSdkVersion(30)
    versionCode = 1
    versionName = "1"
  }
  compileOptions {
    targetCompatibility = JavaVersion.VERSION_1_8
    sourceCompatibility = JavaVersion.VERSION_1_8
  }
  kotlinOptions {
    jvmTarget = "1.8"
  }
  testOptions {
    unitTests.isReturnDefaultValues = true
    unitTests.isIncludeAndroidResources = true
  }
}
dependencies {
  implementation(platform(project(":platform")))
  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
```
Any Android developer would recognize this script, as it is exactly what they'd use if they were using the Kotlin DSL and had simply created the script in place, at something like `android-lib-module/build.gradle.kts`. You can now apply this like so:

**android-lib-module/build.gradle.kts**
```kotlin
plugins {
  `android-library-convention`
}
``` 

And that's it! You can of course customize it as you like, for example by adding dependencies needed by this module (but not needed by _all_ modules, and therefore not part of the convention plugin).

Careful readers will have seen something _slightly_ unusual about the plugin:
```kotlin
dependencies {
  implementation(platform(project(":platform")))
}
```
This refers to a [Java Platform](https://docs.gradle.org/current/userguide/java_platform_plugin.html) project, and is the current best-practice for managing and centralizing dependency versions. Discussion of the Java Platform plugin is outside of the scope of this article, but I include it here as an example of how you can use this convention plugin approach to reduce boilerplate &mdash; without this, you would need to add `implementation(platform(project(":platform")))` to every Android library build script, which is easy to forget.

# The `java-library-convention` plugin

This is very straightforward:

**buildSrc/src/main/kotlin/java-library-convention.gradle.kts**
```kotlin
plugins {
  `java-library`
}

java {
  sourceCompatibility = JavaVersion.VERSION_1_8
  targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
  implementation(platform(project(":platform")))
}
```

And applied as before:
**java-lib-module/build.gradle.kts**
```kotlin
plugins {
  `java-library-convention`
}
```
What I particularly like about this is not having to remember to add `sourceCompatibility` and `targetCompatibility` to every Java library module I have.

# The `kotlin-library-convention` plugin

This is also very straightforward:

**buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts**
```kotlin
plugins {
  id("org.jetbrains.kotlin.jvm")
}

tasks.withType<KotlinCompile>().configureEach {
  kotlinOptions {
    jvmTarget = "1.8"
  }
}

dependencies {
  implementation(platform(project(":platform")))
  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
```

And applied as before:
**kotlin-lib-module/build.gradle.kts**
```kotlin
plugins {
  `kotlin-library-convention`
}
```
I honestly don't even know how many times I've created a new Kotlin module and forgotten to add the stdlib. Everything looks fine in the IDE but good luck getting it to compile!

# Why differentiate?

The reason I have decided to show three separate examples is due to the weight I place upon the values of specificity and minimalism.[<sup>1</sup>](#endnote-1) <a name="source-1" /> We could reduce boilerplate even more if we had stopped after writing our `android-library-convention` plugin. It already knows how to compile Android, Java library, and Kotlin library projects, after all.

Probably the biggest problem with simplifying to that extent is the negative impact it would have on build performance. Building Android libraries is much harder and slower than building Java libraries. Building Kotlin libraries is also slower than building Java libraries. And annotation processing is much slower than not annotation processing! (Please do not apply `kotlin-kapt` unless you're actually in need of Kotlin annotation processing.) **Apply only what you need and no more:** plugins are not free.

From a software architecture perspective, it's also useful to be specific. By applying only a JVM plugin, you are indicating that this is intended as a JVM library, and you make it harder (a good thing!) for the next developer swinging through to import `android.content.Context`.

# From convention plugins to convention tasks

Why stop there? I regularly see people asking about the following scenario:

> I have a heterogeneous build, with Android application modules, Android library modules, and JVM (Java/Kotlin) modules. I want to be able to run something like `./gradlew test` and run all the important test tasks in all the modules. How can I do that?

Let's update our convention plugins to show how to make this scenario easier to handle.

**android-library-convention.gradle.kts**
```kotlin
...

tasks.register("mainTest") {
  dependsOn("testDebugUnitTest") // for example
}
```

**java-library-convention.gradle.kts**
```kotlin
...

tasks.register("mainTest") {
  dependsOn("test")
}
```
(And the same for `kotlin-library-convention`.)

Now you can easily take advantage of one of Gradle's conveniences[<sup>2</sup>](#endnote-2) <a name="source-2" /> and execute

```
./gradlew mainTest
```

When you do this, Gradle will execute the `mainTest` task in every module that has it. If a module doesn't have it, it's fine; there will only be an error if the task isn't present in _any_ module.

It's worth noting that the following would _not_ be advisable:

```
./gradlew test testDebugUnitTest
```

You might think that accomplishes the same thing at the cost of only one more word on the command line, without the trouble of adding the task to each module. Unfortunately, you'll run into the fact that Android projects _do_ have a task named "test"! Here's a portion of the output from executing `./gradlew app:test --dry-run` against my small project (with two build types and two product flavors):

```
...
:app:preProductionReleaseBuild SKIPPED
:app:compileProductionReleaseAidl SKIPPED
:app:compileProductionReleaseRenderscript SKIPPED
:app:generateProductionReleaseBuildConfig SKIPPED
:app:generateProductionReleaseResValues SKIPPED
:app:generateProductionReleaseResources SKIPPED
:app:injectCrashlyticsMappingFileIdProductionRelease SKIPPED
:app:processProductionReleaseGoogleServices SKIPPED
:app:mergeProductionReleaseResources SKIPPED
:app:createProductionReleaseCompatibleScreenManifests SKIPPED
:app:extractDeepLinksProductionRelease SKIPPED
:app:processProductionReleaseManifest SKIPPED
:app:processProductionReleaseResources SKIPPED
:app:kaptGenerateStubsProductionReleaseKotlin SKIPPED
:app:kaptProductionReleaseKotlin SKIPPED
:app:compileProductionReleaseKotlin SKIPPED
:app:javaPreCompileProductionRelease SKIPPED
:app:compileProductionReleaseJavaWithJavac SKIPPED
:app:kaptGenerateStubsProductionReleaseUnitTestKotlin SKIPPED
:app:kaptProductionReleaseUnitTestKotlin SKIPPED
:app:compileProductionReleaseUnitTestKotlin SKIPPED
:app:preProductionReleaseUnitTestBuild SKIPPED
:app:javaPreCompileProductionReleaseUnitTest SKIPPED
:app:compileProductionReleaseUnitTestJavaWithJavac SKIPPED
:app:mergeProductionReleaseShaders SKIPPED
:app:compileProductionReleaseShaders SKIPPED
:app:generateProductionReleaseAssets SKIPPED
:app:mergeProductionReleaseAssets SKIPPED
:app:packageProductionReleaseUnitTestForUnitTest SKIPPED
:app:generateProductionReleaseUnitTestConfig SKIPPED
:app:processProductionReleaseJavaRes SKIPPED
:app:processProductionReleaseUnitTestJavaRes SKIPPED
:app:testProductionReleaseUnitTest SKIPPED
:app:test SKIPPED
```
That is not even one-tenth of it. If you didn't already know it, you'd quickly learn that `test` is basically an alias for every unit test variant.

![Sad dog](https://dev-to-uploads.s3.amazonaws.com/i/r6sunotjibzxio12hx7f.jpg)

As a final note, if you also want to somehow merge all the test reports, that's a slightly more complex problem that I might tackle in a future post.

I hope this small example is enough to show the power and utility of convention plugins and tasks.

# Endnotes
<sup>1</sup><a name="endnote-1"/> Not in endnotes, though. The more the merrier! [<small>up</small>](#source-1)
<sup>2</sup><a name="endnote-2"/> Gradle is famously convenient. [<small>up</small>](#source-2)