Make the most of your Gradle scripts

At the dawn of a new project, the team in charge is in a comfy spot, with no legacy project to refactor. It’s something completely new, a blank slate. And what do you do with a blank-slate project? Use the latest development tools and features. What does this mean if we think about Android builds? A multi-module project with Gradle 7.0 and Gradle Kotlin-DSL.

But these tools are meaningless if we do not take full advantage of them. As in any project, time is against us, and some things get left behind (those that do not add value in the short term). Tests, documentation, Gradle files, and so on are these undervalued ugly ducklings. This creates a problem in the long term — one that gets bigger and bigger, and at some point, the team needs to re-evaluate the situation, take a step back and do things better.

So for this particular endeavor, let’s focus on two challenges to improve our builds:

  • Challenge 1: Avoid redundant configuration in the build.gradle.kts
  • Challenge 2: Central declaration of dependencies with Kotlin-DSL

Challenge 1: Avoid redundant configuration in the build.gradle.kts

In multi-module projects, the most common case is having one app module and several android-library modules, each with its own build.gradle (or build.gradle.kts for those using Kotlin-DSL) files. The more Gradle files you have, the more duplicate code is in them. Think about the android { } configuration block containing SDK versions, build types, compile and testing options, etc. All those options are usually the same for all modules. Redundant code is always bad, but Gradle doesn’t get the same attention as normal code. And things will only get worse when adding more plugins and configurations: JaCoCo, Detekt, Maven-publish plugin, and so on. Trying to solve this with Groovy would have been particularly easy — move everything to a new file, like android.gradle, and simply apply it to the build.gradle in the right place:

The content of android.gradle will be merged with the content of the build.gradle during the build phase. So clean and easy. The bad side? This doesn’t work with Kotlin-DSL because it is strongly typed and needs more context to start configuring the android { } block.

Solution: Share configuration via Gradle plugins

Fortunately, writing a Gradle plugin is not difficult. This will make all the shared configuration between modules not duplicated but instead centralized in a single file (or files for better organization).

In our buildSrc module, create a java folder and add a new package, here called plugin. Inside this package, create the plugins. It should look like this when you finish (with more or fewer plugins depending on your needs):

There are two kinds of plugins:

  • Binary plugins: written programmatically by implementing Plugin interface.
  • Script plugins: additional build scripts.

Script plugins

Create a new Kotlin script file in the folder created before. For example, the plugin script created for ben-manes/gradle-versions plugin can be namedDependencyUpdates.gradle.kts. After moving the configuration, it should look like this:

Now the plugin is ready to be used in other Gradle files. Add it to the plugins { } block:

Binary plugins

Create a new Kotlin class in the folder created before. For example, creating a plugin script for an android-library plugin, you can name the file AndroidLibraryPlugin.kt. The AndroidLibraryPlugin class must implement the Plugin<Gradle> interface. For this reason, the plugin must implement a fun apply(target: Project) method that Gradle will call during the project evaluation phase. There are a lot of extension subclasses for specific modules, for example, an app or an android-library. To configure the Android extension, we need to get the right one. We can find it by type or by name.

Let’s see how to find by name and configure it:

But we can go one step further (finding by type in this example):

Before using the binary plugin, it must be registered. This is done in the build.gradle.kts of our buildSrc module, adding the following code:

Now the plugin is ready to be used in other Gradle files. Add it to the plugins { } block:

That’s all! All shared configurations are in one place: better understood, easier to update, faster in build time, and showing reduced error points.

Challenge 2: Central declaration of dependencies with Kotlin-DSL

There are a lot of ways to share dependency versions between modules in multi-module projects. For example, users can declare versions or dependencies:

  • directly in build scripts (in the ext block)
  • with external files (e.g. dependencies.gradle)
  • in buildSrc
  • using dedicated plugins

But until now, there was no standard mechanism to do this. In Gradle 7.0, you can use version catalogs as an experimental feature. A version catalog allows us to centralize dependencies in a conventional configuration file and declare the actual dependencies in a type-safe way.

In addition to this, instead of declaring repositories in every submodule of your build (or with allprojects { } block), we can declare them in a central place using a central declaration of repositories.

Version catalogs can be declared in the settings.gradle.kts file. We can use the settings API or a TOML file. If a libs.versions.toml file is found in the Gradle subdirectory of the root build, then a catalog will be automatically declared with the contents of this file. Other TOML files can be added but need to be declared explicitly. The central declaration of dependencies also requires the activation of the VERSION_CATALOGS feature preview.

By default, repositories declared by a project will override this, but you can force the build to fail if repositories are declared on projects.

You need to add your dependencies to the TOML file. Versions and dependency declaration can either be declared as simple strings or using rich versions. Look at an example with different approaches:

Now you can change all declared dependencies in the Gradle files to the new system. Also, remove duplicate entries by taking advantage of the bundles. And that’s all:

Conclusions

In our case, X-Apps is a multi-module with one app module and three android-library modules, each with its own gradle.build.kts file.

The benefit, in terms of code duplication, is easy to see by comparing the number of lines of code before and after the changes:

The app module only takes advantage of one bundle from the version catalog, so the reduction is lower. But the difference in our other files is pretty impressive. We also reduced the build time (with this and other Gradle changes) from 4 minutes and 40 seconds to 1 minute and 30 seconds. So don’t forget to take care of your Gradle files — your project will shine even more.