Product Flavor w programowaniu na Androida to mechanizm Gradle, który pozwala tworzyć wiele wariantów jednej aplikacji z wspólnej bazy kodu. Każdy flavor może mieć własny applicationId, zasoby, zależności i funkcjonalność — na przykład darmową i płatną wersję. Według Google Android Developers, 2025, Product Flavors wchodzą w skład systemu Build Variants i łączą się z Build Types poprzez flavorDimensions. To standardowe podejście do publikowania wielu wersji aplikacji w Google Play.
Najważniejsze
Product Flavor to konfiguracja Gradle w bloku android.productFlavors, która opisuje wariant produktu. Każdy flavor może nadpisywać applicationId, versionName, versionCode, minSdkVersion, targetSdkVersion, signingConfig i inne parametry defaultConfig. Product Flavors nie mają ograniczeń ilościowych: projekt może zawierać 2, 5, 10 flavor-ów — Gradle przetworzy wszystkie kombinacje.
Product Flavor rozwiązuje problem codebase reuse — gdy z jednego repozytorium trzeba zbudować kilka różniących się aplikacji. Typowe scenariusze: darmowa wersja z reklamami i płatna bez; wersja demo z ograniczoną funkcjonalnością; wersje korporacyjna i konsumencka; white-label aplikacje dla różnych klientów. Bez Product Flavors każdą wersję trzeba by utrzymywać w osobnym projekcie, co prowadzi do duplikacji kodu w 60-70%.
Historycznie Product Flavors pojawiły się w Android Gradle Plugin 0.9 (2013) jako zamiennik konfiguracji ant. Przedtem programiści używali osobnych projektów dla różnych wersji lub ręcznej zamiany zasobów przed kompilacją. Wprowadzenie flavor-ów w AGP zunifikowało podejście i uczyniło je standardem. Według ankiety JetBrains, 2024, 78% projektów Android z wieloma wersjami używa Product Flavors, reszta — ręcznego przełączania przez BuildConfig lub reflection.
Build Type zarządza procesem kompilacji (debug z debugowaniem, release z optymalizacją). Product Flavor zarządza zawartością kompilacji (free bez płatnych funkcji, paid z nimi). Build Type to konfiguracja infrastrukturalna, Product Flavor — produktowa. Oba pojęcia są ortogonalne: debugowa kompilacja free-flavor różni się od release’owej free-flavor tylko parametrami kompilacji, ale nie funkcjonalnością. Product Flavor nie można używać do wyłączania debuggera — to zadanie Build Type.
Flavor Dimensions (wymiary) to mechanizm grupowania Product Flavors w niezależne kategorie. Jeśli aplikacja ma wersję darmową/płatną oraz osobno region amerykański/europejski, flavour grupuje się w dwa wymiary: „tier” (free, paid) i „region” (us, eu). Gradle tworzy iloczyn kartezjański wymiarów: freeUs, freeEu, paidUs, paidEu — 4 warianty. Bez wymiarów Gradle postrzegałby wszystkie cztery flavour jako jedną płaszczyznę i można by wybrać tylko jeden.
Wymiary deklaruje się w bloku flavorDimensions jako ciąg lub listę ciągów. Kolejność wymiarów wpływa na priorytet source sets: pierwszy wymiar ma najwyższy priorytet. Jeśli wymiar A (tier) jest pierwszy, to src/free/ będzie nadpisywać src/us/ w przypadku konfliktu zasobów. Kolejność wpływa również na nazwę Variant: najpierw flavour pierwszego wymiaru, potem drugiego, a następnie Build Type: freeUsDebug.
Liczba wymiarów nie jest ograniczona, ale każdy nowy wymiar mnoży liczbę Build Variants. Dla projektu z 4 wymiarami (po 2 flavour) i 2 build types otrzymamy 2 × 2 × 2 × 2 × 2 = 32 warianty. Praktyczny limit to 3 wymiary (maksymalnie 8-12 wariantów). Więcej — a konfiguracja Gradle zwalnia, a w Android Studio panel Build Variants staje się nieczytelny.
android {
flavorDimensions "tier", "api"
productFlavors {
free {
dimension "tier"
applicationId "com.example.app.free"
versionNameSuffix "-free"
}
paid {
dimension "tier"
applicationId "com.example.app.paid"
}
minApi21 {
dimension "api"
minSdk 21
}
minApi26 {
dimension "api"
minSdk 26
}
}
}
// Rezultat: freeMinApi21, freeMinApi26, paidMinApi21, paidMinApi26
// Każdy × debug/release = 8 Build Variants
Aby utworzyć Product Flavor, należy dodać blok productFlavors wewnątrz android, podać nazwę flavor i jego parametry. Minimalna deklaracja flavor to nazwa i dimension. Wszystkie pozostałe parametry są dziedziczone z defaultConfig i mogą być nadpisane. Flavor dziedziczy defaultConfig w całości, łącznie z applicationId, versionCode i testInstrumentationRunner.
Każdy flavor może nadpisywać applicationId — pozwala to na zainstalowanie kilku wersji aplikacji na jednym urządzeniu jednocześnie. Na przykład wersja free będzie com.example.app.free, a paid — com.example.app.paid. Jeśli applicationId nie zostanie nadpisane, wszystkie flavour będą mieć ten sam identyfikator i nie będzie można zainstalować ich równolegle. applicationId musi zgadzać się z package w manifeście (jeśli nie używa się applicationIdSuffix).
AGP 8+ zaleca używanie Kotlin DSL zamiast Groovy dla build.gradle. Kotlin DSL zapewnia bezpieczny typowo dostęp do konfiguracji: IDE podpowiada nazwy parametrów, sprawdza typy na etapie kompilacji i podświetla błędy. Migracja z Groovy na Kotlin DSL dla Product Flavors zwykle polega na zastąpieniu cudzysłowów nawiasami i dodaniu typów. AGP jest wstecznie zgodne — oba składnie działają równolegle w jednym projekcie.
// build.gradle.kts — Kotlin DSL
android {
flavorDimensions += "tier"
productFlavors {
register("free") {
dimension = "tier"
applicationId = "com.example.app.free"
versionNameSuffix = "-free"
buildConfigField("boolean", "IS_PREMIUM", "false")
}
register("paid") {
dimension = "tier"
applicationId = "com.example.app.paid"
versionNameSuffix = "-paid"
buildConfigField("boolean", "IS_PREMIUM", "true")
}
}
}
Każdy Product Flavor tworzy własny source set — katalog src/<flavorName>/. W tym katalogu można umieszczać nadpisane zasoby, źródła i manifest. Source set flavor działa jak nakładka na main: pliki z src/free/res/ nadpisują pliki z src/main/res/ o tych samych nazwach. Pozwala to na różne napisy, ikony, kolory i układy dla każdego flavor bez zmiany głównego kodu.
Do nadpisywania klas Java/Kotlin istnieją dwa podejścia: flavor-specific implementation (implementacja abstrakcyjnej klasy w każdym flavor) i BuildConfig field (rozgałęzienie w kodzie). Pierwsze podejście jest czystsze: definiujesz interfejs lub abstrakcyjną klasę w main, a konkretne implementacje w src/free/ i src/paid/. Podczas kompilacji kompilowana jest tylko implementacja bieżącego flavor. Daje to jednoczesne korzyści: mniejszy rozmiar APK (kod płatny nie trafia do wersji free) i bezpieczeństwo (nie ma możliwości przypadkowego wywołania płatnej funkcji).
AndroidManifest.xml w source set flavor nie zastępuje, ale scala się z głównym manifestem. Scalanie odbywa się według zasad Androida: te same atrybuty w elemencie są nadpisywane, unikalne — dodawane. Na przykład, jeśli w głównym manifeście zadeklarowano INTERNET permission, a w free — nie, internet pozostanie. Ale tools:node="replace" pozwala zastąpić cały blok manifestu dla konkretnego flavor. Jest to przydatne, gdy różne flavour wymagają różnych uprawnień (zapis na SD dla paid, kamera dla free).
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="Free App"
tools:replace="android:label">
</application>
</manifest>
Rozważmy typowy scenariusz: free — wersja z reklamami i podstawowymi funkcjami, paid — bez reklam, z rozszerzoną funkcjonalnością. Dla wersji free ustawia się applicationId „com.example.app.free”, dla paid — „com.example.app.paid”. Obie wersje mogą być zainstalowane na jednym urządzeniu jednocześnie, ponieważ applicationId jest unikalnym identyfikatorem aplikacji w systemie Android.
Architektonicznie podział opiera się na interface + flavor implementation. W głównym source set deklaruje się interfejs PaymentService. W src/free/ znajduje się implementacja, która wyświetla reklamy przed płatnością przez AdMob. W src/paid/ — implementacja, która od razu przechodzi do bramki płatniczej. Kod używający PaymentService nie wie, która implementacja jest załadowana — rozstrzyga się to na etapie kompilacji. Takie podejście gwarantuje, że do wersji free nie trafi kod zarządzania subskrypcjami, nawet jeśli programista przypadkowo go wywoła.
Rozmiar APK dla różnych flavor może się różnić o 5-15 MB ze względu na dołączanie/wykluczanie zależności. Aby wykluczyć bibliotekę z konkretnego flavor, używa się flavor-specific dependencies w build.gradle: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0'. Ta zależność zostanie dodana tylko dla wariantu free i nie zwiększy rozmiaru wersji paid. Dla wspólnych zależności używa się implementation — są one dołączane we wszystkich flavor.
// src/main/kotlin/com/example/payment/PaymentService.kt
interface PaymentService {
fun processOrder(amount: Double, callback: (PaymentResult) -> Unit)
}
// src/free/kotlin/.../FreePaymentService.kt
class FreePaymentService : PaymentService {
override fun processOrder(amount: Double, callback: (PaymentResult) -> Unit) {
AdManager.showInterstitial {
PaymentGateway.charge(amount, callback)
}
}
}
// src/paid/kotlin/.../PaidPaymentService.kt
class PaidPaymentService : PaymentService {
override fun processOrder(amount: Double, callback: (PaymentResult) -> Unit) {
PaymentGateway.charge(amount, callback)
}
}
W projektach wielomodułowych moduły biblioteczne mogą nie mieć własnych Product Flavors, co stwarza problem: biblioteka jest kompilowana raz (jako release), a moduł app z flavor oczekuje biblioteki z odpowiednim wariantem. Od AGP 8.1 biblioteki mogą publikować multiple variants przez blok publishing.multipleVariants — pozwala to opublikować wszystkie warianty flavor biblioteki do jednego repozytorium maven, a moduł app automatycznie wybierze odpowiedni.
Alternatywnym podejściem jest zadeklarowanie tych samych flavorDimensions i productFlavors w bibliotece co w module app. AGP automatycznie dopasowuje flavour według pełnej zgodności nazwy w jednym wymiarze. Jeśli nazwa flavor w bibliotece zgadza się z nazwą w app, AGP utworzy spójne warianty. Dla łatwiejszego utrzymania zaleca się umieszczenie wspólnych definicji flavour w Convention Plugin — wtyczce Gradle, która jest stosowana we wszystkich modułach projektu.
Dla bibliotek nieprzeznaczonych do publikacji (wewnętrzne moduły) wystarczy zsynchronizować flavour przez build.gradle głównego projektu. Gradle udostępnia metodę subprojects, która pozwala zastosować konfigurację do wszystkich podprojektów. Należy jednak pamiętać, że zbyt duża konfiguracja w subprojects spowalnia fazę konfiguracji. Zaleca się używanie Convention Plugins — są one kompilowane raz i wielokrotnie używane, co skraca czas konfiguracji o 15-30%.
Często zadawane pytania
Nie ma ograniczeń ilościowych, ale każdy wymiar mnoży liczbę Build Variants. 4 flavour w jednym wymiarze + 2 build types = 8 wariantów. 4 + 4 w dwóch wymiarach = 16 wariantów. Zaleca się nie więcej niż 3 wymiary i 10-12 łącznych wariantów.
Tak, przez source set src/<flavor>/AndroidManifest.xml. Manifest scala się z głównym. Do zastąpienia całego bloku użyj tools:node="replace". Na przykład, aby zmienić etykietę aplikacji lub uprawnienia dla konkretnego flavor.
Użyj konfiguracji <flavorName>Implementation. Przykład: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0'. Taka zależność zostanie dołączona tylko podczas kompilacji wariantu free. Dla paid: paidImplementation. Wspólne zależności podaje się przez implementation.
Product Flavor określa wersję produktu (free, paid, demo), Build Type — sposób kompilacji (debug, release). Flavour mogą nadpisywać applicationId, versionName, zasoby. Build Type zarządza debuggable, minification, signing. Oba są ortogonalne i łączą się w Build Variant.
Tak, Product Flavors działają z Compose bez ograniczeń. Różne flavor mogą mieć różne ekrany Compose przez source sets lub implementację abstrakcyjnych klas. Można też dodawać zależności Compose specyficzne dla flavor: freeImplementation 'androidx.compose.ui:ui-tooling'.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również