P
Pratham Arora
Guest

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 β
- Reduce the boilerplate code for setting up a new module.
- Maintain certain commonalities between similar modules.
- Manage SDK, JDK, Project Flavors, Lint etc. level information centrally.
- 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?

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
Things to note in this are β
- The modules can be categorized under Feature, Domain, Data and Core/Shared functionalities. (This can vary based upon your setup)
- 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

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).

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.

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

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).

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-
- Reduced boilerplate code for existing and new modules by abstracting repetitive configurations into reusable plugins.
- Improved reusability and maintainability of app and module-level build settings using Kotlin DSL and version catalogs.
- Centralized dependency and SDK configuration, ensuring consistency across all modules.
- Faster onboarding for new team members or contributors, as the plugin structure defines conventions and best practices.
- Clear separation of concerns between app-level and module-level responsibilities in the build logic.
Next Steps
- 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.
- Take the idea forward and implement such plugins for KMM projects.
- For better abstraction, you may choose to push your custom plugins to maven, jfrog etc.
References
- https://docs.gradle.org/current/userguide/writing_plugins.html
- https://medium.com/@magicbluepenguin/how-to-create-your-first-custom-gradle-plugin-efc1333d4419
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...