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
appmodule - Android library
lib1module - Android library
lib2module
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-dslin 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"
- Convention plugins are just regular plugins
- A "convention plugin" is a plugin that only your team uses
- 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")andjava-gradle-pluginare interchangable. Same withmaven-publishSee 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)
}
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")
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"
}
}
}
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())
}
}
}
}
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"
}
and
plugins {
id("libtest.android.library")
}
android {
namespace = "com.cidle.lib2"
}
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()
}
}
Done!
Thank you again Martin Bonnin for all of the teaching on this subject!
Top comments (0)