Managing Android Multi-module Project with Gradle Plugin and Kotlin

Malvin Sutanto
Wantedly Engineering
5 min readDec 9, 2019
Photo by Jan Kolar on Unsplash

Lately, more and more teams are adopting a multi-module setup for their Android projects. The number of modules for each project can vary, and some projects can even have more than a hundred modules. Managing these modules involves a lot of copying and sharing common settings and codes between build.gradle files. Some teams use separate Gradle files and apply from: statements to try to solve this issue. However, it is not very scalable once you exceed a certain number of files.

In this post, I will introduce how we can make use of a custom Gradle plugin to help us manage Android project modules. We will also provide some customizability into our plugin so that each module can customize the build parameters. Then we will also see how we can integrate other plugins, e.g., Jacoco, from our multi-module plugin. This is how our build.gradle.kts file will roughly look like at the end.

build.gradle.kts file for an Android module with custom Gradle plugin applied.

Compare this with a typical build.gradle file that roughly achieves the same thing:

Typical build.gradle file for an Android module.

This implementation is made using Android plugin 3.6.0-beta04 and Gradle 6.0.1

Gradle Plugin with Kotlin

Let’s look at some examples of what the plugin can do for each module:

  • Applies important and required plugins, such as kotlin-android and kotlin-android-extensions.
  • Configures common android module settings for a module (minSdkVersion, targetSdkVersion, etc.) but still allows each module to override any settings.
  • Specifies the default proguard files.
  • Enables supported Java 8 features across all modules.
  • Adds required dependencies to a module, such as kotlin-stdlib
  • Configures Jacoco for each module and allow custom configuration block that supports both kotlin and groovy script.

Now, before creating the Gradle plugin, first, we need to create a buildSrc/build.gradle.kts file in the root folder and add the required dependencies.

buildSrc/build.gradle.kts file.

Since we declared the dependency to both android-gradle-plugin and kotlin-gradle-plugin in this file, we can remove the classpath dependencies to both artifacts from our root/build.gradle.

And then in buildSrc/src/main/kotlin, we will create the plugin class that implements the Plugin interface.

Extending the Plugin interface.

The apply method will be invoked in the project evaluation step of Gradle build lifecycle and it will be the entry point into our plugin.

Apply Other Plugins

Since we want our plugin to automatically apply kotlin-android and kotlin-android-extensions to our modules, we simply add apply plugin statements to our project like so:

Applying other plugins through custom Gradle plugin.

Configure Common Android Settings

There are many settings that can be shared between an application, library, test, and feature module. Since we want this plugin to manage the configuration between all type of modules, we are going to make use of a class named BaseExtension from the Android Gradle plugin.

Setting common Android build properties.

You can also apply custom configuration for a specific type of module by using the respective extension type, e.g., AppExtension for an application module.

Default Proguard Files

We also want the plugin to automatically pick-up the correct proguard files for each module’s release configuration, be it for an application or a library module.

Declaring proguard files to use based on the module type.

Enable Supported Java 8 Features

To enable Java 8 features for each module, we need to set the sourceCompatibility, targetCompability, and kotlinOptions.jvmTarget. Setting the jvmTarget for Kotlin is not that straightforward, but it’s not that difficult either.

Enabling Supported Java 8 Features.

Adding Required Dependencies

Sometimes, there are libraries that we need across different modules. Some libraries that come to mind arekotlin-std-lib, androidx-core, and testing libraries. Adding the dependencies to them through the plugin is very simple:

Adding external Gradle artefact dependencies through dependencies block.

Jacoco and Custom Settings Through Extension

Setting up a Jacoco task for each module can be quite complicated since you have to write and register the task on each module. Our plugin provides a good opportunity to set-up a simple and unified way to introduce coverage report for each module. As an example, let’s say we want to have the following settings that we want each module to specify:

  1. Whether to generate the coverage report for this module.
  2. File pattern for classes to be excluded from each module’s coverage report.
android { ... }myOptions {
jacoco {
isEnabled = true
excludes(
"file/pattern/to**Exclude**"
)
}
}

To allow custom configuration for the plugin, we first need to create an extension class that declares the configurable parameters.

Extension class that declares the configurable Jacoco parameters.

For it to work with Gradle API, we need to make our classes and members open. Also, we’re going to use Action class from Gradle API so that it will play nicely with Groovy scripts. It is a good idea to initialize the extensions and options to some sane defaults in order to minimize the required configurations in each module. For example here, code coverage is enabled by default on all modules, but each individual module can disable it through its build.gradle file.

We will then register the extension in our plugin to make the configuration block available in the build script:

Declaring extension for customizability.

And to read the configuration values that have been set by each module, we will have to use Project.afterEvaluate block since the values are set during the evaluation step of our modules.

Reading extension class’ properties.

If everything is set up correctly, we will now be able to invoke :module:jacocoDebugReport (or jacocoFlavorDebugReport) Gradle task that will run the unit tests and then generate a coverage report for us.

Final Steps

Now that the Gradle plugin is completed, we can then register the plugin using gradlePlugin block in buildSrc/build.gradle.kts so that it is available in our module’s build.gradle file.

Registering a Gradle plugin.

Finally, we apply the plugin into each module and add overrides or custom configurations as we see fit.

Other Benefits of Using Gradle Plugin

With this Gradle plugin approach, our build scripts are now relatively shorter and easier to migrate to Kotlin DSL. Also, Gradle plugin can be applied to both Groovy and Kotlin DSL, allowing us to migrate our build scripts module by module based on our needs instead of converting all of them at once.

After the build.gradle files have been migrated into kts, we can make use of extension functions (declared in buildSrc/src/main/kotlin) to simplify our build files even further, like so:

Extension function to declare dependencies on external Gradle artefacts.

Managing a multi-module project with a custom Gradle plugin is good for common Gradle script configurations that need to be applied across different modules, making build.gradle scripts simpler and more maintainable. It also provides a single location where we can define and manage common parameters like targetSdkVersion and minimumSdkVersion while also allowing each module to define its own overrides.

Custom Gradle plugin also provides an opportunity for us to extend the capability of a Gradle script through custom extensions and Kotlin DSL. Allowing us to create a more complex build configuration while still keeping it easy to read. Moreover, with Kotlin DSL, we can further simplify our build script with extension functions.

You can find the sample implementation of this plugin in the following repository:

Thank you for reading this post. I hope you found it useful!

--

--