16 Grundlagen von Build-Varianten

16.1 Konzept und Motivation

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.

16.2 Build-Typen als Basis

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
    }
}

16.3 Produkt-Flavors für funktionale Varianten

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.

16.4 Source-Sets und Ressourcen-Management

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.

16.5 Dimension-Strategien für komplexe Varianten

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.

16.6 Abhängigkeiten und Varianten

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.