Build-Varianten ermöglichen es, aus einer gemeinsamen Codebasis mehrere unterschiedliche Versionen einer Anwendung zu erstellen. Dieses Konzept adressiert die Anforderung, verschiedene Ausprägungen einer Software für unterschiedliche Zielumgebungen, Kundengruppen oder Einsatzzwecke zu generieren, ohne den Quellcode duplizieren zu müssen. Eine typische Anwendung benötigt beispielsweise eine Debug-Version für die Entwicklung, eine Test-Version für das QA-Team und eine Release-Version für die Produktion.
Der traditionelle Ansatz, solche Varianten über Branches oder separate Projekte zu verwalten, führt zu erhöhtem Wartungsaufwand und Inkonsistenzen. Build-Varianten lösen dieses Problem durch parametrisierte Build-Konfigurationen. Der gemeinsame Code bleibt in einer einzigen Codebasis, während variantenspezifische Anpassungen über Konfiguration gesteuert werden.
Build-Typen definieren grundlegende Konfigurationsvarianten einer Anwendung. Die häufigsten Build-Typen sind Debug und Release. Ein Debug-Build enthält zusätzliche Logging-Informationen, Debug-Symbole und deaktivierte Optimierungen für bessere Fehleranalyse. Der Release-Build optimiert für Performance und Größe, aktiviert ProGuard oder R8 für Code-Obfuskation und Minimierung.
In Gradle werden Build-Typen im buildTypes-Block
konfiguriert. Jeder Build-Typ kann eigene Compiler-Flags,
Signing-Konfigurationen und Build-Properties definieren. Die
Konfiguration erfolgt hierarchisch: Ein Build-Typ erbt
Standardeinstellungen und überschreibt nur die spezifischen
Eigenschaften. Diese Vererbungshierarchie reduziert
Konfigurationsduplikation und macht Build-Varianten wartbar.
buildTypes {
debug {
minifyEnabled false
debuggable true
applicationIdSuffix ".debug"
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
signingConfig signingConfigs.release
}
}Während Build-Typen technische Varianten darstellen, repräsentieren Produkt-Flavors funktionale oder geschäftliche Varianten. Ein Produkt-Flavor könnte eine kostenlose versus eine kostenpflichtige Version der Anwendung sein, verschiedene Brandings für unterschiedliche Kunden oder regional angepasste Versionen mit spezifischen Features.
Produkt-Flavors werden im productFlavors-Block definiert
und können eigene Ressourcen, Abhängigkeiten und Quellcode-Verzeichnisse
haben. Jeder Flavor erhält ein eigenes Source-Set unter
src/flavorName/, in dem flavor-spezifische
Implementierungen abgelegt werden. Gradle merged diese
flavor-spezifischen Ressourcen und Code-Dateien automatisch mit dem
Haupt-Source-Set.
Die Kombination aus Build-Typen und Produkt-Flavors erzeugt die finalen Build-Varianten. Bei zwei Build-Typen (debug, release) und zwei Flavors (free, paid) entstehen vier Build-Varianten: freeDebug, freeRelease, paidDebug und paidRelease. Jede Variante kann separat gebaut und deployed werden.
Build-Varianten nutzen ein ausgeklügeltes System von Source-Sets zur
Organisation variantenspezifischer Code- und Ressourcen-Dateien. Das
Haupt-Source-Set unter src/main/ enthält den gemeinsamen
Code aller Varianten. Build-Typ-spezifische Source-Sets wie
src/debug/ oder src/release/ ergänzen
typspezifische Implementierungen. Flavor-spezifische Source-Sets unter
src/free/ oder src/paid/ fügen
flavor-spezifische Features hinzu.
Die Merge-Reihenfolge dieser Source-Sets folgt klaren Regeln.
Ressourcen werden von der spezifischsten zur allgemeinsten Ebene
überschrieben. Eine Datei in src/paidRelease/ überschreibt
die gleiche Datei in src/paid/, src/release/
und src/main/. Für Java- oder Kotlin-Code gilt, dass
Klassen nicht überschrieben, sondern nur ergänzt werden können. Eine
Klasse kann entweder im Haupt-Source-Set oder in einem
variantenspezifischen Source-Set existieren, aber nicht in beiden.
Diese Struktur ermöglicht präzise Kontrolle über variantenspezifische Anpassungen. Eine Debug-Variante kann erweiterte Logging-Klassen enthalten, während die Release-Variante optimierte Implementierungen verwendet. Ressourcen wie Konfigurationsdateien, Grafiken oder Strings können pro Variante angepasst werden, ohne Codeduplikation zu erzeugen.
Für komplexere Anforderungen unterstützt Gradle Flavor-Dimensions. Dimensions ermöglichen die Organisation von Flavors in orthogonale Kategorien. Eine Dimension könnte die Zielplattform (phone, tablet, tv) definieren, während eine andere Dimension das Geschäftsmodell (free, premium, enterprise) abbildet. Die Kombination aller Dimensions mit allen Build-Typen erzeugt das kartesische Produkt aller möglichen Varianten.
flavorDimensions "platform", "tier"
productFlavors {
phone {
dimension "platform"
}
tablet {
dimension "platform"
}
free {
dimension "tier"
applicationId "com.example.free"
}
premium {
dimension "tier"
applicationId "com.example.premium"
}
}Mit zwei Platforms, zwei Tiers und zwei Build-Typen entstehen acht Build-Varianten. Gradle generiert Tasks für jede Variante automatisch. Die Variant-API ermöglicht programmatischen Zugriff auf Varianten-Konfigurationen, um beispielsweise bestimmte Kombinationen zu deaktivieren oder variantenspezifische Tasks zu registrieren.
Build-Varianten können eigene Abhängigkeiten definieren. Die
Dependency-Configuration folgt dem Namensschema
variantNameImplementation oder
flavorNameImplementation. Eine paid-Variante kann
zusätzliche Libraries für Premium-Features einbinden, während die
free-Variante diese Abhängigkeiten nicht hat. Diese granulare Kontrolle
über Abhängigkeiten reduziert die finale Anwendungsgröße und vermeidet
das Einbinden ungenutzter Libraries.
Die Auflösung von Abhängigkeiten erfolgt hierarchisch. Gradle sucht zuerst nach variantenspezifischen Abhängigkeiten, dann nach flavor-spezifischen, dann nach build-typ-spezifischen und schließlich nach allgemeinen Abhängigkeiten. Diese Hierarchie ermöglicht sowohl allgemeine als auch hochspezifische Dependency-Konfigurationen ohne Redundanz.