← Back to list

[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…

Padam Chopra · 2025-07-29 14:03 · 2 claps · 3.0 min read
#android #gradle-plugin #gradle #android-app-development #android-modular-app
Open on Medium ↗

[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