Reducing boilerplate in Gradle multi-module projects

Remco Mokveld
ProAndroidDev
Published in
5 min readMay 4, 2018

--

Edit 2022–06–17: This post was published in 2018 and, in my personal opinion, didn’t age very well. I’ll make an attempt to update it at some point I would recommend looking into concepts like convention plugins instead of following the recommendations in this article

Build time is one of the biggest perils of any Android developer. One method of optimizing build times is by splitting your build up into multiple modules (or in gradle terminology, sub-projects). However, this involves quite some extra build logic which also needs to be maintained. In this blog post I will discuss a method of greatly reducing the amount of Gradle code required in multi-project builds.

When you have a single Gradle project it is usually set up with a rootProject and a single sub-project. The root project declares a couple of configurations which are the same for all the sub-projects, and the sub-project build.gradle declares configurations for that specific project.

Single project configuration

In the single project set-up Android Studio generates three files

// $PROJECT_DIR$/build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath ‘com.android.tools.build:gradle:3.1.2’
}
}
allprojects {
repositories {
google()
jcenter()
}
}

A single sub-project named app.

// $PROJECT_DIR$/app/build.gradle
apply plugin: ‘com.android.application’
android {
compileSdkVersion 27
defaultConfig {
applicationId “com.example.app”
minSdkVersion 14
targetSdkVersion 27
versionCode 1
versionName “1.0”
}
}
dependencies {
implementation ‘com.google.dagger:dagger:2.15’
annotationProcessor ‘com.google.dagger:dagger-compiler:2.15’
}

And a file which defines app to be a sub-project of the root project.

// $PROJECT_DIR$/settings.gradle
include ':app'

This is quite a concise configuration, there is barely any duplication and it is easy to read. However, when you start adding more sub-projects, you will need to create the second file for every project you add. This file will look almost identical for all sub-projects and when something changes for all sub-projects, it needs to be changed in all of those files.

There are multiple blog posts out there which propose concepts like declaring versions (or while dependency declarations) in the rootProject’s ext properties. But this still requires quite a lot of duplicate configuration. You still need to tell every subproject that it should use a constant, defined in rootProject.ext as it’s compileSdkVersion.

Another way, to prevent having to declare configurations multiple times is by using the subprojects closure in the rootProject’s build.gradle. This closure will be called for every project which is defined in settings.gradle. So let’s say we have an app sub-project, player, dashboard and news feature sub-projects, and a core sub-project. All sub-projects depend on the core project, and app depends on all other sub-projects. In the rest of this article I will describe a clean and concise way of setting up that project.

What makes a gradle project?

But first, to understand this setup it is important to explain what a Gradle project actually is.

A Gradle project starts with a root project which get’s configured using the build.gradle in that folder. Before a project’s build.gradle is evaluated though, Gradle looks for a settings.gradle which can define any sub-projects for the rootProject. These sub-projects are the same as a rootProject with the exception that they are not required to contain a build.gradle file. Then Gradle will look for project files for every the sub-project in a directory that corresponds to the name of the sub-project.

As per our example, this:

include ‘:app’, ‘:core’, ‘:player’, ‘:dashboard’, ‘:news’

Now, since sub-projects act the same as the root project, in theory you could have a app/settings.gradle to define sub-projects for that `app` sub-project but that is not really relevant for this post. It is however relevant, that the app project can be declared without the app folder actually containing a build.gradle.

For every sub-project a closure named subprojects is invoked with this referencing the sub-project.

Using this subprojects closure, we could define our original single project build like this.

// $PROJECT_DIR$/build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath ‘com.android.tools.build:gradle:3.1.2’
}
}
allprojects {
repositories {
google()
jcenter()
}
}
subprojects {
// within this closure the implicit “it” refers to
// the sub-project, and acts as the default receiver.
apply plugin: ‘com.android.application’ android {
compileSdkVersion 27
defaultConfig {
applicationId “com.example.app”
minSdkVersion 14
targetSdkVersion 27
versionCode 1
versionName “1.0”
}
}

dependencies {
implementation ‘com.google.dagger:dagger:2.15’
annotationProcessor ‘com.google.dagger:dagger-compiler:2.15’
}
}

With the same settings.gradle and no $PROJECT_DIR$/app/build.gradle required.

// $PROJECT_DIR$/settings.gradle
include ':app'

This is functionally equivalent to the the original three files. Adding a new sub-project now is as simple as updating setting.gradle to include ‘:app’, ‘:core’. This means that now there is a gradle project in $PROJECT_DIR$/app/ and in $PROJECT_DIR/core/.

However, now for both of those the application plugin is applied, while `core` should be a library. We can fix this by adding some extra logic to the subprojects closure. We can replace apply plugin: com.android.application with:

if (this.name == ‘app’) {
// remember, “this” refers to the sub-project.
apply plugin: ‘com.android.application’
} else {
apply plugin: ‘com.android.library’
}

This means we now have one rootProject and two sub-projects, app and core and the only gradle files we have are $PROJECT_DIR$/build.gradle and project/settings.gradle, and adding more sub-projects is as easy as updating settings.gradle. You do still need to create an AndroidManifest for every android sub-project since the plugin requires you to define a package-name, but you would need to do that in any case.

Defining dependencies for all projects

Next we need to define dependencies for each sub-project. In this case all except for `core` need to have an `implementation` dependency on `:core`, and let’s say we also use dagger, then all projects need to have the dagger dependencies. The snippet below shows what we can add to the `subprojects` closure to achieve this.

// $PROJECT_DIR$/build.gradle
subprojects {
// … apply plugins and android config …

dependencies {
// Add a dependency on core to all projects except
// for core itself.
if (this.name != ‘core’)
implementation project(‘:core’)
implementation ‘com.google.dagger:dagger:2.15’
annotationProcessor ‘com.google.dagger:dagger-compiler:2.15’
}
}

Although you could define some of the dagger dependencies in core as api dependencies instead, annotationProcessor dependencies are not transitive and there need to be declared for every project. Besides that I find it kinda nice that it is now very clear that dagger is a dependency for all sub-projects instead of a transitive thing that only gets declared in core.

Project specific configurations.

Now in theory, with enough if/else statements you can define your entire build logic in the root build.gradle, but you don’t want to put too much config in there which is specific to a single sub-project. It is always difficult to draw a line for what should be in global and what should be project specific. Here are some example of how you can do stuff project specific.

For example, the app project must declare an applicationId. This can be done by specifying the following app/build.gradle.

// $PROJECT_DIR$/app/build.gradle
android.defaultConfig.applicationId = ‘com.example.app’

The same can be done if, for example, the player uses exoplayer.

// $PROJECT_DIR$/player/build.gradle
dependencies {
implementation “com.google.android.exoplayer:exoplayer:r2.7.3”
}

Dependency version constants

Although this post provides a different way of centralizing your project gradle configuration than defining dependency versions in ext, this is still quite a nice way of preventing duplication and can be used hand in hand with the subprojects approach.

--

--