Build-Logik organisieren: Convention Plugins in Android-Projekten
Convention Plugins bündeln wiederholte Gradle-Konfiguration zentral. So bleiben auch große Multi-Modul-Projekte konsistent und wartbar.
Sobald ein Android-Projekt über ein einzelnes Modul hinauswächst, wiederholt sich Gradle-Konfiguration auf unangenehme Weise: Compiler-Optionen, Lint-Regeln, AGP-Versionen und Compose-Einstellungen tauchen in jeder build.gradle.kts-Datei nahezu identisch auf. Build-Logik-Organisation ist die Praxis, genau diese Duplikate zu eliminieren und an einem einzigen, versionierbaren Ort zu bündeln.
Was ist das?
Build-Logik-Organisation beschreibt, wie du gemeinsame Gradle-Konfiguration so strukturierst, dass sie sich nicht in jedem Modul wiederholt. Das zentrale Werkzeug dafür sind Convention Plugins – auch precompiled script plugins genannt. Dabei handelt es sich um gewöhnliche Gradle-Plugins, die du innerhalb deines Projekts selbst definierst und dann in beliebigen Modulen anwendest, genau wie ein offizielles Plugin aus Maven Central.
Im Android-Kontext bedeutet das konkret: Statt in zehn Feature-Modulen zehnmal compileSdk = 35, minSdk = 24 und kotlinOptions { jvmTarget = "17" } zu schreiben, fasst du diese Einstellungen in einem Plugin wie android-library-convention zusammen. Ändert sich der Compile-SDK, passt du genau eine Datei an – alle Module folgen automatisch.
Dieses Prinzip ist besonders wertvoll in der Phase „Modern Android Architecture”. Saubere Modul-Grenzen zwischen Presentation-, Domain- und Data-Layer sind nur dann langfristig wartbar, wenn alle Module dieselben Build-Regeln teilen. Abweichungen in einzelnen Modulen führen sonst zu schwer nachvollziehbaren Inkonsistenzen bei Lint, Tests oder dem Release-Build.
Wie funktioniert es?
Es gibt zwei verbreitete Orte, um Convention Plugins unterzubringen:
buildSrc ist ein spezielles Verzeichnis, das Gradle automatisch als Classpath-Dependency behandelt. Es ist der schnellste Einstieg, hat aber einen entscheidenden Nachteil: Jede Änderung in buildSrc invalidiert den Build-Cache aller Gradle-Tasks im gesamten Projekt.
build-logic als Included Build ist die modernere Lösung und wird in den offiziellen Android-Architektur-Empfehlungen bevorzugt. Du erstellst ein separates Gradle-Projekt im Unterordner build-logic und trägst in der Haupt-settings.gradle.kts ein:
// settings.gradle.kts (Wurzelverzeichnis)
includeBuild("build-logic")
Innerhalb von build-logic legst du unter src/main/kotlin/ Kotlin-Dateien an. Jede Datei mit der Endung .gradle.kts wird automatisch zu einem anwendbaren Plugin – der Dateiname bestimmt den Plugin-Bezeichner:
build-logic/
src/
main/
kotlin/
android-application-convention.gradle.kts
android-library-convention.gradle.kts
android-compose-convention.gradle.kts
In android-library-convention.gradle.kts könnte die zentrale Konfiguration so aussehen:
// build-logic/src/main/kotlin/android-library-convention.gradle.kts
plugins {
id("com.android.library")
kotlin("android")
}
android {
compileSdk = 35
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
Ein Feature-Modul wendet das Plugin mit einer einzigen Zeile an:
// feature/home/build.gradle.kts
plugins {
id("android-library-convention")
}
Alles andere – compileSdk, minSdk, Kotlin-Version – erbt das Modul automatisch aus dem Convention Plugin.
In der Praxis
Der häufigste erste Schritt ist, die Compose-spezifische Konfiguration auszulagern, weil sie in fast jedem UI-Modul identisch anfällt. Convention Plugins dürfen dabei andere Convention Plugins als Basis verwenden:
// android-compose-convention.gradle.kts
plugins {
id("android-library-convention")
}
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
}
}
dependencies {
val bom = libs.androidx.compose.bom
implementation(platform(bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
debugImplementation(libs.androidx.compose.ui.tooling)
}
Typische Stolperfalle: Du erstellst das build-logic-Verzeichnis, schreibst deine Plugins – aber Gradle erkennt sie nicht. Der häufigste Grund: includeBuild("build-logic") fehlt in der Haupt-settings.gradle.kts oder steht nach dem dependencyResolutionManagement-Block. Gradle liest settings.gradle.kts sequenziell; includeBuild muss vor dependencyResolutionManagement erscheinen, damit der Plugin-Classpath korrekt aufgebaut wird.
Eine zweite Falle betrifft den Zugriff auf den Versionskatalog: Die build-logic-eigene settings.gradle.kts braucht einen eigenen versionCatalogs-Block, der auf die libs.versions.toml des Hauptprojekts zeigt, sonst stehen libs.*-Accessors in deinen Convention Plugins nicht zur Verfügung:
// build-logic/settings.gradle.kts
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
Kombinierst du Convention Plugins mit einer libs.versions.toml-Versionskatalog-Datei, hast du eine konsistente Build-Grundlage, die versioniert, reviewbar und für das gesamte Team nachvollziehbar ist.
Fazit
Convention Plugins sind das entscheidende Werkzeug, um Gradle-Konfiguration in wachsenden Android-Projekten unter Kontrolle zu halten. Statt zehn Module einzeln anzupassen, änderst du eine Datei – und alle Betroffenen ziehen nach. Überprüfe jetzt dein eigenes Projekt: Wie viele build.gradle.kts-Dateien wiederholen denselben compileSdk- oder kotlinOptions-Block? Wähle einen dieser Blöcke aus, extrahiere ihn in ein Convention Plugin, und stelle im Code-Review sicher, dass alle Modul-Builds weiterhin grün bleiben. Das ist der direkteste Weg, um zu spüren, wie viel Aufwand zentralisierte Build-Logik im Alltag einspart.