Maintainable Gradle Scripts for Multi-Module Android Projects

P

Pratham Arora

Guest
1*BZdfNG40Mgqkq8MJl0PnvQ.jpeg

Credits to Hans-Peter Gauster

What to expect from this blog?​


Working on an ever growing Android project leads to creation of multiple modules for better separation of concerns. With this, comes the responsibility of maintaining several Gradle scripts for each module. Considering that the modules could belong to maybe feature, domain, design or data layerβ€Šβ€”β€Šit adds up to keeping certain properties confined to specific modules.

In this blog, I am focusing on how one could create App-level as well as Module-level Gradle plugins to β€”

  1. Reduce the boilerplate code for setting up a new module.
  2. Maintain certain commonalities between similar modules.
  3. Manage SDK, JDK, Project Flavors, Lint etc. level information centrally.
  4. Adds the possibility of further abstracting Gradle files to cloud.

In Sortly’s production android app, we are moving towards breaking down monolith structures into well defined modules. It made sense for us to reduce the efforts of maintaining Gradle files while we modularize the rest of the code.

How does the sample project look like?​

1*uIGIU7LKvjsnABva-jolHw.png

Multi modular setup for Sample project

The Blog Sample App contains features, local storage, network and design system modules. Complexity of your project could vary based upon your scale and use case.

Dependency graph of the sample project

Dependency graph of the Sample project

Things to note in this are β€”

  1. The modules can be categorized under Feature, Domain, Data and Core/Shared functionalities. (This can vary based upon your setup)
  2. With great number of modules, comes great responsibility πŸ•·

Here is current structure of app and module level gradle files.

Getting started with a custom plugin πŸš€

Step 1: Add a new directory


Create a directory at the top level of your project. You can name it however you like. For this blog, I am gonna go ahead with build-common. Add the reference to this directory in the settings.gradle.kts

1*5VaFyBvRuwO8EE9odtVIqQ.png


includeBuild tells Gradle to include another Gradle build as a sibling to your main project directory. And, it treats build-common as a valid Gradle project (with its own build.gradle.kts and settings.gradle.kts files).

1*jZIfWb3xCOljFhh7is4-CQ.png


Sync your Gradle files.

Step 2: Add the necessary files to your module​


Start by adding settings.gradle.kts to your module followed by a new directory with the following hierarchy.

1*TckDxH-QnFh8uBCOMSvI7g.png


Add a build.gradle.kts in your properties folder. You can choose to rename the top level folder (properties) as per your convenience.

1*MtpkvD7oeJ4k9hDI5u0U5g.png

Step 3: Setting up settings.gradle.kts


Add the following code to the file. This will help our module to resolve project dependencies, access version catalog from main app, and include properties module while building. Sync your Gradle files.

pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

include("properties")

Step 4: Setting up build.gradle.kts​


Add the following dependencies, if already not present in your libs.versions.toml

android-tools-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

Now, let’s add these dependencies in our build.gradle.kts along with the kotlin-dsl, which will enable us to write Kotlin-based logic in our gradle files.

Note: compileOnly helps in resolving the dependencies at compile time. This is an important bit for the setup.

plugins {
`kotlin-dsl`
}

dependencies {
compileOnly(libs.android.tools.build.gradle)
compileOnly(libs.kotlin.gradle.plugin)
}

Step 5: Gearing up before writing the plugin​


Let’s create 2 packages in the properties module (namely extension and plugins).

1*mdIapdoPtaIBnG34GLdCfg.png


We will be moving targetSdk, compileSdk and minSdk fields from build.gradle.kts -> libs.versions.toml. Along with that, we can also move version code and version name.

[versions]
android-compileSdk = "36"
android-minSdk = "29"
android-targetSdk = "36"
app-version-code = "1"
app-version-name = "1.0"

Now, let’s add an extension function to access these values in our module. We can create a file under our extension package and name it ProjectExtensions.kt. Add the following code to this file.

fun Project.getLibsFromVersionCatalog(): VersionCatalog {
return extensions
.findByType(VersionCatalogsExtension::class.java)?.named("libs")
?: error("Version catalog 'libs' not found. Please ensure it is defined in your settings.gradle.kts")
}

Step 6: Starting with the App Level Plugin​


Under the plugins package, lets add a new Kotlin class named AppLevelPlugin.kt and extend it with Plugin (on Project)

class AppLevelPlugin: Plugin<Project> {

override fun apply(project: Project) = with(project) {
// Plugin code goes here...
}
}

Make sure to import the classes from org.gradle.api package.

As mentioned above, this is how our current App Level build.gradle.kts looks like.

Let’s start by moving the plugins block…

class AppLevelPlugin: Plugin<Project> {

override fun apply(project: Project) = with(project) {
// Import the libs from version catalog
val libs = getLibsFromVersionCatalog()
// Extension function on Project to import plugins
configurePlugins(libs)
}

private fun Project.configurePlugins(libs: VersionCatalog) {
plugins.apply(libs.findPlugin("android-application").get().get().pluginId)
plugins.apply(libs.findPlugin("kotlin-android").get().get().pluginId)
plugins.apply(libs.findPlugin("kotlin-compose").get().get().pluginId)
}
}

Note: The argument name inside findPlugin(β€œ<plugin-name>”) should be the same as mentioned in your libs.versions.toml

Like in my case, it looks like this -

android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

Let’s configure SDK versions, version codes and names by creating extension function on BaseAppModuleExtension.

override fun apply(project: Project) = with(project) {
// Import the libs from version catalog
val libs = getLibsFromVersionCatalog()
// Extension function on Project to import plugins
configurePlugins(libs)

extensions.configure<BaseAppModuleExtension> {
addSdkAndVersion(libs)
}
}

private fun BaseAppModuleExtension.addSdkAndVersion(libs: VersionCatalog) {
compileSdk = libs.findVersion("android.compileSdk").get().requiredVersion.toInt()
defaultConfig {
minSdk = libs.findVersion("android.minSdk").get().requiredVersion.toInt()
targetSdk = libs.findVersion("android.targetSdk").get().requiredVersion.toInt()
targetSdkPreview = libs.findVersion("android.targetSdk").get().requiredVersion.toString()
versionCode = libs.findVersion("app.version.code").get().requiredVersion.toInt()
versionName = libs.findVersion("app.version.name").get().requiredVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}

Similarly, rest of the properties (apart from dependencies) can be shifted our plugin. The final version of the plugin would look something like this:

Step 7: Moving on to the Module Level Plugin​


Under the plugins package, lets add another Kotlin class named ModuleLevelPlugin.kt and extend it with Plugin (on Project). Similar to how we created for the app plugin. Here is the build.gradle.kts file for this module.

This is how the ModuleLevelPlugin.kt would look like. Notice the subtle changes in configuring android-library plugin and creating extensions on LibraryExtension.

Step 8: Registering your plugins​


Moving towards the last step of this modularization is registering both the plugins in libs.version.toml and build-common’s build.gradle.kts

[plugins]
app-level-plugin = { id = "com.pratham.blogsampleapp.app.properties" }
module-level-plugin = { id = "com.pratham.blogsampleapp.module.properties" }

Once this is done, you may remove most of the code from App and Module level gradle files, and include the newly created plugins in them.

Let’s compare how the old and new gradle files look like β€”

+--------------+---------------+---------------+
| | Old | New |
+--------------+---------------+---------------+
| App Level | Link | Link |
| Module Level | Link | Link |
+--------------+---------------+---------------+

Conclusion​


After navigating through the above steps, we can conclude the following findings-

  1. Reduced boilerplate code for existing and new modules by abstracting repetitive configurations into reusable plugins.
  2. Improved reusability and maintainability of app and module-level build settings using Kotlin DSL and version catalogs.
  3. Centralized dependency and SDK configuration, ensuring consistency across all modules.
  4. Faster onboarding for new team members or contributors, as the plugin structure defines conventions and best practices.
  5. Clear separation of concerns between app-level and module-level responsibilities in the build logic.

Next Steps​

  1. You can create separate plugins for modules targeting different responsibilities. For instance, one shared plugin for feature modules, one shared plugin for storage modules and so on.
  2. Take the idea forward and implement such plugins for KMM projects.
  3. For better abstraction, you may choose to push your custom plugins to maven, jfrog etc.

References

  1. https://docs.gradle.org/current/userguide/writing_plugins.html
  2. https://medium.com/@magicbluepenguin/how-to-create-your-first-custom-gradle-plugin-efc1333d4419
stat



Maintainable Gradle Scripts for Multi-Module Android Projects was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue reading...
 


Join 𝕋𝕄𝕋 on Telegram
Channel PREVIEW:
Back
Top