[Android] Supercharging Android Multi-Module Projects with Gradle Plugin Scripts
When working on a growing Android codebase, managing multiple modules can quickly become tedious. You’ll find yourself duplicating…
[Android] Supercharging Android Multi-Module Projects with Gradle Plugin Scripts
When working on a growing Android codebase, managing multiple modules can quickly become tedious. You’ll find yourself duplicating configuration across modules — setting up Kotlin, Android configurations, test options, Jetpack Compose setup, and more.
A better approach is to centralize this logic in custom Gradle plugin scripts. These scripts help you enforce consistency, reduce boilerplate, and make new module creation smoother. This article assumes you have basic knowledge of kotlin gradle scripts and if not, you probably don’t need this setup anyway. We’ll structure our project like this:
root/
├── build-logic/ <--- This directory needs to be created
│ └── settings.gradle.kts
│ └── conventions/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ └── AndroidApplicationConventionPlugin.kt
│ └── AndroidComposeConventionPlugin.kt
│ └── your.package.name
│ └── ExtensionConfigs.kt
│ └── ProjectConfig.kt
│ └── Constants.kt
├── app/
├── your-module-1/
├── .../
├── your-module-n/
└── settings.gradle.kts
🔨 Step 1: Setup the build-logic Module
Create a build-logic module to host all your plugins. To mark it as a gradle plugin directory, you need to edit your root settings.gradle.kts file as follows:
pluginManagement {
includeBuild("build-logic") //add this line
...
}
Then edit build-logic/conventions/build.gradle.kts :
plugins {
`kotlin-dsl`
}
...
dependencies {
compileOnly(libs.android.gradlePlugin) //com.android.tools.build:gradle
compileOnly(libs.kotlin.gradlePlugin) //org.jetbrains.kotlin:kotlin-gradle-plugin
}
tasks {
validatePlugins {
enableStricterValidation = true
failOnWarning = true
}
}
gradlePlugin {
plugins {
register("androidApplication") {
id = "android.application"
implementationClass = "AndroidApplicationConventionPlugin"
}
register("androidCompose") {
id = "android.compose"
implementationClass = "AndroidComposeConventionPlugin"
}
// Register all your plugins here. Read further ahead to
// make more sense of this
}
}
📦 Step 2: Write a Plugin
I will cover two examples here that I had in build-logic setup. First, let’s look at some utility files we need to setup (file structure described above):
// Constants.kt
object Constants {
// environment variables
const val BUILD_CODE = "BUILD_CODE"
const val FALLBACK_BUILD_CODE = 1
const val APP_VERSION_NAME = "APP_VERSION_NAME"
const val FALLBACK_APP_VERSION_NAME = "0.0.0-debug"
// build types
const val BUILD_TYPE_DEBUG = "debug"
// source sets
const val TYPE_DEV_BASE_DIR = "src/dev"
const val TYPE_DEV_SRC_DIR = "$TYPE_DEV_BASE_DIR/java"
const val TYPE_DEV_RES_DIR = "$TYPE_DEV_BASE_DIR/res"
const val TYPE_RELEASE_BASE_DIR = "src/release"
const val TYPE_RELEASE_SRC_DIR = "$TYPE_RELEASE_BASE_DIR/java"
const val TYPE_RELEASE_RES_DIR = "$TYPE_RELEASE_BASE_DIR/res"
}
// ProjectConfig.kt
object ProjectConfig {
const val compileSdkVersion = 36
const val targetSdkVersion = 36
const val minSdkVersion = 28
}
// ExtensionConfigs.kt
internal val Project.libs: VersionCatalog
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")
internal fun BaseExtension.configureDefault() {
// This method configures build codes and versions.
// You can also provide version code and names in your github actions
// to automate build version increments and naming updates
setCompileSdkVersion(ProjectConfig.compileSdkVersion)
with(defaultConfig) {
minSdk = ProjectConfig.minSdkVersion
targetSdk = ProjectConfig.targetSdkVersion
vectorDrawables.useSupportLibrary = true
versionCode = System.getenv(Constants.BUILD_CODE)?.toInt() ?: Constants.FALLBACK_BUILD_CODE
versionName = System.getenv(Constants.APP_VERSION_NAME) ?: Constants.FALLBACK_APP_VERSION_NAME
}
with(compileOptions) {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
}
internal fun BaseExtension.configureBuildTypes() {
sourceSets {
getByName(Constants.BUILD_TYPE_DEBUG) {
manifest.srcFile("${Constants.TYPE_DEV_BASE_DIR}/AndroidManifest.xml")
res.srcDirs(Constants.TYPE_DEV_RES_DIR)
java.srcDirs(Constants.TYPE_DEV_SRC_DIR)
}
}
}
internal fun BaseExtension.configureCompose() {
with(buildFeatures) {
compose = true
}
}
internal fun BaseExtension.disableBuildConfigGeneration() {
with(buildFeatures) {
buildConfig = false
}
}
internal fun Project.configureKotlin() {
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
}
}
internal fun Project.androidDependencies() {
dependencies {
add("coreLibraryDesugaring", libs.findLibrary("desugarJdkLibs").get())
add("implementation", libs.findLibrary("timber").get())
}
}
We are now ready to create our two sample plugins:
// AndroidApplicationConventionPlugin.kt
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
// if your main app module has compose
apply("org.jetbrains.kotlin.plugin.compose")
}
extensions.getByType<AppExtension>().apply {
configureDefault()
configureCompose()
}
configureKotlin()
androidDependencies()
}
}
}
// AndroidComposeConventionPlugin.kt
class AndroidComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.plugin.compose")
}
extensions.getByType<LibraryExtension>().apply {
configureDefault()
configureBuildTypes()
configureCompose()
disableBuildConfigGeneration()
}
configureKotlin()
androidDependencies()
dependencies {
add("implementation", platform(libs.findLibrary("androidx-compose-bom").get()))
add("implementation", libs.findBundle("compose").get())
}
}
}
}
🧼 Step 3: Cleanup app-level build.gradle.kts
plugins {
// plugin id you registered AndroidApplicationConventionPlugin with
id("android.application")
}
android {
namespace = "your.package.name"
defaultConfig {
applicationId = "your.package.name"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies {
...
}
🧪 Step 4: Use It in Modules
Now, any new module you create that requires compose can use our plugin’s id in its build.gradle.kts file:
// feature-module's build.gradle.kts
plugins {
id("android.compose")
}
🧭 Scaling Further with More Convention Plugins
Once you’ve set up this system, it becomes trivial to define specific plugins for different types of modules. This lets you scale your project while maintaining structure and consistency. Here are a few useful examples:
- “android.feature”: Gradle plugin that auto includes design, navigation module, etc.
- “android.cache”: Gradle plugin that auto includes sqldelight/room, serialization, etc.
- “android.network”: Gradle plugin that auto includes your core network module providing retrofit/okhttp setup, serialization, etc.
Example implementation available on GitHub here.
메타데이터
- post_id
- d892fd7e7779
- slug
- android-supercharging-android-multi-module-projects-with-gradle-plugin-scripts-d892fd7e7779
- url
- https://medium.com/@padamchopra/android-supercharging-android-multi-module-projects-with-gradle-plugin-scripts-d892fd7e7779
- canonical_url
- https://medium.com/@padamchopra/android-supercharging-android-multi-module-projects-with-gradle-plugin-scripts-d892fd7e7779
- author_url
- https://medium.com/@padamchopra
- status
- ok
- fetched_at
- 2026-08-18 18:43:50