Recently I got to know about convention plugins in gradle, it eliminates duplicated build logic across multi-module projects. This guide shows you how to enforce consistency, improve performance, and scale your Gradle builds with an included build setup.
Convention Plugins in Gradle: Streamlining Multi-Module Builds
Gradle is the backbone of many modern JVM, Android, and Kotlin projects. As projects grow—especially multi-module ones—build scripts balloon with duplicated configuration for dependencies, compiler options, Android settings, testing, linting, and publishing. This leads to maintenance nightmares, inconsistencies, and slower builds.
Convention plugins solve this elegantly. They are a recommended Gradle pattern for sharing and enforcing build conventions across projects without the pitfalls of subprojects {} / allprojects {} blocks or duplicated script plugins.
A convention plugin is typically a precompiled script plugin (or a binary plugin) that applies and configures core/community plugins with your team’s defaults (for example - Java/Kotlin version, Android SDK settings, common dependencies).
id("my.convention.android-library")).Gradle docs emphasize them as the preferred way to share build logic.
minSdk, compiler flags, test runners).Common pain points they solve: version management (pair with Version Catalogs), boilerplate Android configs, and third-party plugin setup (for example - Kotlin, Compose, KSP, Detekt).
buildSrc (simplest, great for small/medium projects): Automatically available. Limited to one build.build-logic/): More scalable, composite build.We’ll focus on the included build approach, as it’s the current best practice.
Assume a multi-module Android project with a Version Catalog (gradle/libs.versions.toml).
build-logic Included BuildProject structure:
.
├── build-logic/
│ ├── settings.gradle.kts
│ ├── build.gradle.kts
│ └── convention/ # or src/main/kotlin for precompiled
│ └── src/main/kotlin/
├── gradle/libs.versions.toml
├── settings.gradle.kts # root
└── app/, library1/, etc.
build-logic/settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
build-logic/build.gradle.kts (for a binary plugin):
plugins {
`java-gradle-plugin`
alias(libs.plugins.kotlin.jvm)
}
java {
toolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
}
dependencies {
compileOnly(libs.android.gradlePlugin) // or whatever plugins you need
implementation(gradleKotlinDsl())
}
gradlePlugin {
plugins {
register("androidLibrary") {
id = "com.example.convention.android.library"
implementationClass = "com.example.convention.AndroidLibraryConventionPlugin"
}
// Add more: androidApplication, jvmLibrary, etc.
}
}
build-logic/convention/src/main/kotlin/com/example/convention/AndroidLibraryConventionPlugin.kt:
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.*
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.android.library")
extensions.configure<LibraryExtension> {
compileSdk = 36 // from catalog or constant
defaultConfig {
minSdk = 24
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
}
// Add Kotlin, Compose, namespace handling, etc.
}
// Common dependencies via Version Catalog
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("implementation", libs.findLibrary("androidx-core-ktx").get())
// ... more commons
}
}
}
settings.gradle.ktspluginManagement {
includeBuild("build-logic")
repositories { ... }
}
// In root build.gradle.kts, declare plugins with apply false if needed
library1/build.gradle.kts:
plugins {
id("com.example.convention.android.library")
}
android {
namespace = "com.example.library1"
// Override specifics if needed
}
That’s it—modules become tiny and consistent.
myconvention { enableCompose() }) so modules can opt into extras..gradle.kts files in build-logic/convention/src/main/kotlin/ (filename becomes plugin ID).compileOnly for plugins you apply inside; implementation for helpers.--configuration-cache)..kt classes) offers more power (custom tasks, extensions); precompiled is quicker to start.Convention plugins transform chaotic build scripts into clean, declarative, and maintainable code. They align with Gradle’s vision for scalable builds and pay dividends as your project grows.
Start small: Extract one common configuration (for example - Android library basics) into a plugin, then expand. Your future self (and teammates) will thank you.
Further Reading:
What convention would you extract first in your project? Hit me up on LinkedIn!
tags: Android - Gradle - Build - System