DEV Community

ColtonIdle
ColtonIdle

Posted on

How to create a "convention" plugin for your multi-module Android app

One thing that trips me up all the time is how to common-ize my build files in a multi-module Android app. Every time I try to learn it, I give up because of overloaded terms, potential footguns, and possible slowdowns in my app. IMO this should be a lot easier, so I end up just duplicating code and sometimes I will just have some sort of subprojects.all{} block in my root build.gradle to apply something to all of my modules instead.

I'm trying to learn once more with a very simple case where I have:

  • Android app module
  • Android library lib1 module
  • Android library lib2 module

And I want to extract the common code in the android libraries (lib1 and lib2)

Some general notes:

  • According to https://docs.gradle.org/current/userguide/best_practices_structuring_builds.html#favor_composite_builds) that buildSrc isn't recommended and so I should go down a path of a convention plugin for sharing build logic
  • Convention plugin is a loaded term and you can have convention plugins in both buildSrc and build-logic . Similarly, you can write your convention plugins as "precompiled script plugins" (.kts ) or regular "binary plugins" (.kt)
  • https://github.com/autonomousapps/gradle-glossary is a good resource to brush up on gradle terms
  • NowInAndroid saved ~12s in some cases by removing precompiled script plugins
  • If you want the fastest possible performance, you want to publish your convention plugins (annoying for your "typical" android app) (see here)
  • If you see kotlin-dsl in your build, you should try to eliminate it to save some speed
  • Read https://mbonnin.net/2025-07-10_the_case_for_kgp/
  • Many definitions of a "convention plugin"
    1. Convention plugins are just regular plugins
    2. A "convention plugin" is  a plugin that only your team uses
    3. A "convention plugin" is a plugin that you share in your build, and so you could say every plugin is a convention plugin, but typically "convention plugins" are understood as being part of your repo
  • testing
  • id("java-gradle-plugin") and java-gradle-plugin are interchangable. Same with maven-publish See here

This was like 90% done put together with help from Martin Bonnin, but I had to write it down so I don't forget it

Conversion

So let's just pretend we did file > new project, then added two new android lib modules (lib1 and lib2). By default we'll have duplicate code in the the two lib modules. (this is default code that AS new module wizard will generate in Jan of 2026)

plugins {
  alias(libs.plugins.android.library)
}

android {
  namespace = "com.cidle.lib1"
  compileSdk {
    version = release(36)
  }

  defaultConfig {
    minSdk = 27

    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    consumerProguardFiles("consumer-rules.pro")
  }

  buildTypes {
    release {
      isMinifyEnabled = false
      proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
    }
  }
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
  }
}

dependencies {
  implementation(libs.androidx.core.ktx)
  implementation(libs.androidx.appcompat)
  implementation(libs.material)
  testImplementation(libs.junit)
  androidTestImplementation(libs.androidx.junit)
  androidTestImplementation(libs.androidx.espresso.core)
}
Enter fullscreen mode Exit fullscreen mode

Steps

1: Create build-logic directory
2: Add settings.gradle.kts in build-logic and fill it with

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

rootProject.name = "build-logic"
include(":convention")
Enter fullscreen mode Exit fullscreen mode

3: Add a convention dir inside of build-logic dir
4: Inside of this new convention dir create a build.gradle.kts

plugins {
    `kotlin-dsl`
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
}

gradlePlugin {
    plugins {
        register("androidLibrary") {
            id = "libtest.android.library"
            implementationClass = "AndroidLibraryConventionPlugin"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

TODO: Investigate if we can remove kotlin-dsl
5: Then in convention dir, create new package and class structure of src/main/kotlin/AndroidLibraryConventionPlugin.kt

import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType

class AndroidLibraryConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
            }

            extensions.configure<LibraryExtension> {
                compileSdk = 36

                defaultConfig {
                    minSdk = 27
                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                    consumerProguardFiles("consumer-rules.pro")
                }

                buildTypes {
                    release {
                        isMinifyEnabled = false
                        proguardFiles(
                            getDefaultProguardFile("proguard-android-optimize.txt"),
                            "proguard-rules.pro"
                        )
                    }
                }

                compileOptions {
                    sourceCompatibility = JavaVersion.VERSION_11
                    targetCompatibility = JavaVersion.VERSION_11
                }
            }

            val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")

            dependencies {
                add("implementation", libs.findLibrary("androidx-core-ktx").get())
                add("implementation", libs.findLibrary("androidx-appcompat").get())
                add("implementation", libs.findLibrary("material").get())
                add("testImplementation", libs.findLibrary("junit").get())
                add("androidTestImplementation", libs.findLibrary("androidx-junit").get())
                add("androidTestImplementation", libs.findLibrary("androidx-espresso-core").get())
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

TODO: Check to see if there's a better way to use our toml file here. I'm not fond of libs.findLibrary, etc.
6: Update :lib1 and :lib2 respective build.gradle.kts to be

plugins {
  id("libtest.android.library")
}

android {
  namespace = "com.cidle.lib1"
}
Enter fullscreen mode Exit fullscreen mode

and

plugins {
  id("libtest.android.library")
}

android {
  namespace = "com.cidle.lib2"
}
Enter fullscreen mode Exit fullscreen mode

We basically went down from 37 lines to 6 lines... in 2 modules! So for every time we add a new module, we save at least those 30 lines and as our "base" android library definition expands (adds more dependencies, lint configuration, etc) then you save yourself from having to re-write those lines too.

7: In your settings.gradle.kts you need to add one line to add this new build-logic module as an "included build"

pluginManagement {
  includeBuild("build-logic") <===== this is the line you add!
  repositories {
    google {
      content {
        includeGroupByRegex("com\\.android.*")
        includeGroupByRegex("com\\.google.*")
        includeGroupByRegex("androidx.*")
      }
    }
    mavenCentral()
    gradlePluginPortal()
  }
}
Enter fullscreen mode Exit fullscreen mode

Done!

Thank you again Martin Bonnin for all of the teaching on this subject!

Top comments (0)