Product Flavor در توسعهٔ Android مکانیسم Gradle است که اجازه میدهد چند واریانت از یک اپلیکیشن از یک پایگاه کد مشترک ایجاد کنید. هر flavor میتواند applicationId، منابع، وابستگیها و قابلیتهای خود را داشته باشد — مثلاً، نسخهٔ رایگان و پرداختی. به گزارش Google Android Developers, 2025، Product Flavors بخشی از سیستم Build Variants هستند و از طریق flavorDimensions با Build Types ترکیب میشوند. این رویکرد استانداردی برای منتشر کردن چند نسخه از یک اپلیکیشن در Google Play است.
نکات کلیدی
Product Flavor یک پیکربندی Gradle در بلوک android.productFlavors است که یک واریانت محصول را توصیف میکند. هر flavor میتواند applicationId، versionName، versionCode، minSdkVersion، targetSdkVersion، signingConfig و سایر پارامترهای defaultConfig را بازنویسی کند. Product Flavors محدودیت تعداد ندارند: پروژه میتواند 2، 5 یا 10 flavor داشته باشد — Gradle تمام ترکیبات را پردازش خواهد کرد.
Product Flavor مسئلهٔ codebase reuse را حل میکند — وقتی از یک رپوزیتوری باید چند اپلیکیشن متفاوت ساخت. سناریوهای معمولی: نسخهٔ رایگان با تبلیغات و نسخهٔ پرداختی بدون آن؛ نسخهٔ دمو با قابلیت محدود؛ نسخههای شرکتی و مصرفکننده؛ اپلیکیشنهای white-label برای مشتریان مختلف. بدون Product Flavors هر نسخه باید در یک پروژهٔ جداگانه حفظ میشد که منجر به 60-70% تکرار کد میشود.
از نظر تاریخی، Product Flavors در Android Gradle Plugin 0.9 (2013) به عنوان جایگزین پیکربندیهای ant ظاهر شدند. پیش از آن، توسعهدهندگان از پروژههای جداگانه برای نسخههای مختلف یا جایگزینی دستی منابع قبل از ساخت استفاده میکردند. ورود flavor-ها در AGP رویکرد را یکپارچه کرد و آن را به یک استاندارد تبدیل کرد. بر پایه نظرسنجی JetBrains, 2024، 78% پروژههای Android با چند نسخه از Product Flavors استفاده میکنند و باقی — از تغییر دستی از طریق BuildConfig یا reflection.
Build Type فرآیند ساخت را مدیریت میکند (debug با اشکالزدایی، release با بهینهسازی). Product Flavor محتوای ساخت را مدیریت میکند (free بدون قابلیتهای پرداختی، paid با آنها). Build Type یک تنظیم زیرساختی است، Product Flavor یک تنظیم محصولی. هر دو مفهوم مستقیل هستند: ساخت debug از free-flavor با ساخت release از free-flavor تنها در پارامترهای کامپایل تفاوت دارد، نه در قابلیت. Product Flavor نمیتواند برای غیرفعال کردن اشکالزدایی استفاده شود — این وظیفهٔ Build Type است.
Flavor Dimensions (ابعاد) — مکانیسمی برای گروهبندی Product Flavors در دستههای مستقل است. اگر اپلیکیشن نسخهٔ رایگان/پرداختی و بهطور جداگانه منطقهٔ آمریکایی/اروپایی داشته باشد، flavour در دو بعد گروهبندی میشوند: «tier» (free, paid) و «region» (us, eu). Gradle حاصلضرب دکارتی ابعاد را ایجاد میکند: freeUs، freeEu، paidUs، paidEu — 4 واریانت. بدون ابعاد، Gradle هر چهار flavour را یک صفحه واحد در نظر میگرفت و تنها یکی قابل انتخاب بود.
ابعاد در بلوک flavorDimensions به صورت یک رشته یا لیستی از رشتهها اعلان میشوند. ترتیب ابعاد بر اولویت source sets تأثیر میگذارد: بعد اول بالاترین اولویت را دارد. اگر بعد A (tier) اول باشد، در صورت تعارض منابع، src/free/ بر src/us/ اولویت خواهد داشت. همچنین ترتیب بر نامگذاری Variant تأثیر میگذارد: ابتدا flavour بعد اول، سپس بعد دوم، سپس Build Type: freeUsDebug.
تعداد ابعاد محدود نیست، اما هر بعد جدید تعداد Build Variants را چند برابر میکند. برای پروژهای با 4 بعد (هر کدام 2 flavour) و 2 build types: 2 × 2 × 2 × 2 × 2 = 32 واریانت. حد عملی — 3 بعد (حداکثر 8-12 واریانت). بیشتر از آن — پیکربندی Gradle کند میشود و پانل Build Variants در Android Studio غیرقابل خوانش میشود.
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
}
}
}
// نتیجه: freeMinApi21, freeMinApi26, paidMinApi21, paidMinApi26
// هر کدام × debug/release = 8 Build Variants
برای ایجاد Product Flavor، باید بلوک productFlavors را داخل android اضافه کرده، نام flavor و پارامترهای آن را مشخص کنید. حداقل اعلان flavor نام و dimension است. سایر پارامترها از defaultConfig به ارث میرسند و میتوانند بازنویسی شوند. Flavor defaultConfig را کاملاً به ارث میبرد، از جمله applicationId، versionCode و testInstrumentationRunner.
هر flavor میتواند applicationId را بازنویسی کند — این امکان نصب چند نسخه از اپلیکیشن را بر یک دستگاه بهطور همزمان فراهم میکند. مثلاً، نسخهٔ free com.example.app.free و paid com.example.app.paid خواهد بود. اگر applicationId بازنویسی نشود، همه flavour شناسه یکسانی خواهند داشت و نصب موازی آنها ممکن نخواهد بود. applicationId باید با package در مانیفست مطابقت داشته باشد (اگر applicationIdSuffix استفاده نشوده باشد).
AGP 8+ به جای Groovy برای build.gradle استفاده از Kotlin DSL را توصیه میکند. Kotlin DSL دسترسی ایمن از نظر نوع به پیکربندی فراهم میکند: IDE نام پارامترها را پیشنهاد میدهد، انواع را در مرحله کامپایل بررسی میکند و خطاها را برجسته میکند. مهاجرت از Groovy به Kotlin DSL برای Product Flavors معمولاً شامل جایگزینی گیومهها با پرانتز و افزودن انواع است. AGP به عقب مواظب است — هر دو نحو در یک پروژه بهصورت موازی کار میکنند.
// 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")
}
}
}
هر Product Flavor یک source set خود را ایجاد میکند — پوشهٔ src/<flavorName>/. در این پوشه میتوان منابع، منابع و مانیفست بازنویسی شده را قرار داد. Source set flavor به عنوان یک لایه بر روی main عمل میکند: فایلهای src/free/res/ فایلهای src/main/res/ با همان نامها را بازنویسی میکنند. این امکان رشتهها، آیکونها، رنگها و چیدمانهای مختلف را برای هر flavor بدون تغییر کد اصلی فراهم میکند.
برای بازنویسی کلاسهای Java/Kotlin دو روش وجود دارد: flavor-specific implementation (پیادهسازی کلاس انتزاعی در هر flavor) و BuildConfig field (شاخه در کد). روش اول تمیزتر است: شما یک رابط یا کلاس انتزاعی در main تعریف میکنید و پیادهسازیهای مشخص را در src/free/ و src/paid/. در زمان ساخت، تنها پیادهسازی flavor فعلی کامپایل میشود. این مزایای همزمان را فراهم میکند: حجم APK کوچکتر (کد پرداختی وارد نسخهٔ free نمیشود) و امنیت (امکان صدا زدن اکراهی تابع پرداختی وجود ندارد).
AndroidManifest.xml در source set flavor جایگزین نمیشود، بلکه با مانیفست main ادغام مییابد. ادغام بر اساس قوانین Android انجام میشود: ویژگیهای یکسان در یک عنصر بازنویسی میشوند، ویژگیهای منحصربهفرد اضافه میشوند. مثلاً، اگر در مانیفست main مجوز INTERNET اعلان شده باشد ولی در free نباشد، اینترنت بقایی خواهد داشت. اما tools:node="replace" به شما اجازه میدهد که کل بلوک مانیفست را برای یک flavor خاص جایگزین کنید. این وقتی مفید است که flavourهای مختلف به مجوزهای مختلفی نیاز داشته باشند (نوشتن روی SD برای paid، دوربین برای 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>
یک سناریوی معمولی را بررسی میکنیم: free — نسخه با تبلیغات و قابلیتهای پایه، paid — بدون تبلیغات، با قابلیت گسترده. برای نسخهٔ free، applicationId «com.example.app.free» و برای paid — «com.example.app.paid» تنظیم شده است. هر دو نسخه میتوانند بهصورت همزمان روی یک دستگاه نصب شوند، زیرا applicationId شناسه یکتای اپلیکیشن در سیستم Android است.
از نظر معماری، تقسیم بر اساس interface + flavor implementation ساخته شده است. در source set main، رابط PaymentService اعلان میشود. در src/free/ پیادهسازی قرار دارد که از طریق AdMob تبلیغات را قبل از پرداخت نشان میدهد. در src/paid/ — پیادهسازی که مستقیماً به درگاه پرداخت میرود. کدی که از PaymentService استفاده میکند نمیداند کدام پیادهسازی بارگذاری شده — این در مرحله کامپایل تعیین میشود. چنین رویکردی تضمین میکند که کد مدیریت اشتراک وارد نسخهٔ free نشود، حتی اگر توسعهدهنده آن را بهطور اتفاقی صدا بزند.
حجم APK برای flavorهای مختلف میتواند 5-15 مگابایت تفاوت داشته باشد به دلیل وابستگیهای وارد/خارج شده. برای حذف یک کتابخانه از یک flavor خاص، از flavor-specific dependencies در build.gradle استفاده میشود: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0'. این وابستگی تنها برای واریانت free اضافه میشود و حجم نسخهٔ paid را افزایش نمیدهد. برای وابستگیهای مشترک، implementation استفاده میشود که در تمام 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)
}
}
در پروژههای چندماژولی، ماژولهای کتابخانهای ممکن است Product Flavors خود را نداشته باشند، که مسئلهای ایجاد میکند: کتابخانه یک بار (به عنوان release) ساخته میشود، اما ماژول app با flavor انتظار نسخه موافق کتابخانه را دارد. از AGP 8.1، کتابخانهها میتوانند از طریق بلوک publishing.multipleVariants چندین واریانت منتشر کنند — این امکان انتشار تمام واریانتهای flavor کتابخانه را در یک مخزن maven فراهم میکند و ماژول app بهصورت خودکار نسخه مورد نیاز را انتخاب میکند.
رویکرد جایگزین — اعلان همان flavorDimensions و productFlavors در کتابخانه مانند ماژول app است. AGP بهصورت خودکار flavour را بر اساس تطابق کامل نام در یک بعد مطابقت میدهد. اگر نام flavor در کتابخانه با نام در app مطابقت داشته باشد، AGP واریانتهای هماهنگ ایجاد خواهد کرد. برای تسهیل نگهداری، توصیه میشود تعریفهای مشترک flavour در Convention Plugin — یک افزوده Gradle که به تمام ماژولهای پروژه اعمال میشود، قرار داده شوند.
برای کتابخانههایی که برای انتشار در نظر گرفته نشدهاند (ماژولهای داخلی)، کافی است flavour را از طریق build.gradle پروژه ریشه هماهنگ کنید. Gradle روش subprojects را ارائه میدهد که اجازه اعمال پیکربندی را به تمام زیرپروژهها میدهد. اما به خاطر داشته باشید که پیکربندی بسیار زیاد در subprojects فاز پیکربندی را کند میکند. توصیه میشود از Convention Plugins استفاده کنید — آنها یک بار کامپایل شده و مجدداً استفاده میشوند که زمان پیکربندی را 15-30% کاهش میدهند.
سوالات متداول
محدودیت تعداد وجود ندارد، اما هر بعد تعداد Build Variants را چند برابر میکند. 4 flavour در یک بعد + 2 build types = 8 واریانت. 4 + 4 در دو بعد = 16 واریانت. توصیه میشود حداکثر 3 بعد و 10-12 واریانت داشته باشید.
بله، از طریق source set src/<flavor>/AndroidManifest.xml. مانیفست با اصلی ادغام میشود. برای جایگزینی کل بلوک از tools:node="replace" استفاده کنید. مثلاً، تغییر برچسب اپلیکیشن یا مجوزها برای یک flavor خاص.
از پیکربندی <flavorName>Implementation استفاده کنید. مثال: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0'. این وابستگی تنها در ساخت واریانت free وارد خواهد شد. برای paid: paidImplementation. وابستگیهای مشترک از طریق implementation مشخص میشوند.
Product Flavor نسخهٔ محصول (free, paid, demo) را مشخص میکند، Build Type — روش ساخت (debug, release). Flavour میتوانند applicationId، versionName و منابع را بازنویسی کنند. Build Type debuggable، minification و signing را مدیریت میکند. هر دو مستقل هستند و در Build Variant ترکیب میشوند.
بله، Product Flavors بدون محدودیت با Compose کار میکنند. Flavorهای مختلف میتوانند از طریق source sets یا پیادهسازی کلاسهای انتزاعی صفحات Compose مختلفی داشته باشند. همچنین میتوان وابستگیهای Compose مخصوص flavor را اضافه کرد: freeImplementation 'androidx.compose.ui:ui-tooling'.
نتیجهگیری
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.