Product Flavor في تطوير Android هو آلية في Gradle تسمح بإنشاء متغيرات متعددة لنفس التطبيق من قاعدة بيانات مشتركة. يمكن أن يكون لكل flavor applicationId خاص به، وموارد، وتبعيات، ووظائف — على سبيل المثال، إصدارات مجانية ومدفوعة. وفقًا لـ Google Android Developers, 2025، تعد Product Flavors جزءًا من نظام Build Variants ويتم دمجها مع Build Types من خلال flavorDimensions. هذا هو النهج القياسي لنشر إصدارات متعددة من التطبيق على Google Play.
النقاط الرئيسية
Product Flavor هو تكوين Gradle في الكتلة android.productFlavors الذي يصف متغير المنتج. يمكن لكل flavor تجاوز applicationId وversionName وversionCode وminSdkVersion وtargetSdkVersion وsigningConfig ومعلمات defaultConfig الأخرى. ليس لدى Product Flavors حد للكمية: يمكن أن يحتوي المشروع على 2 أو 5 أو 10 flavors — يتعامل Gradle مع جميع التركيبات.
Product Flavor يحل مشكلة إعادة استخدام قاعدة البيانات (codebase reuse) — عندما تحتاج إلى بناء تطبيقات متعددة مختلفة من مستودع واحد. السيناريوهات النموذجية: إصدار مجاني مع إعلانات وإصدار مدفوع بدونها؛ إصدار تجريبي بوظائف محدودة؛ إصدارات مؤسسية واستهلاكية؛ تطبيقات white-label لعملاء مختلفين. بدون Product Flavors، يجب صيانة كل إصدار في مشروع منفصل، مما يؤدي إلى تكرار الكود بنسبة 60-70%.
تاريخيًا، ظهرت Product Flavors في Android Gradle Plugin 0.9 (2013) كبديل لتكوينات ant. قبل ذلك، استخدم المطورون مشاريع منفصلة لإصدارات مختلفة أو استبدال الموارد يدويًا قبل البناء. أدى إدخال flavors في 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 في فئات مستقلة. إذا كان للتطبيق إصدار مجاني/مدفوع وبشكل منفصل منطقة أمريكية/أوروبية، يتم تجميع الـ flavors في بُعدين: "tier" (free, paid) و"region" (us, eu). ينشئ Gradle المنتج الديكارتي للأبعاد: freeUs, freeEu, paidUs, paidEu — 4 متغيرات. بدون أبعاد، سيتعامل Gradle مع الأربعة flavors كمستوى واحد، ويمكن اختيار واحد فقط.
يتم تعريف الأبعاد في الكتلة flavorDimensions كسلسلة أو قائمة سلاسل. يؤثر ترتيب الأبعاد على أولوية source sets: البُعد الأول له الأولوية القصوى. إذا تم تحديد البُعد A (tier) أولاً، فإن src/free/ سيتجاوز src/us/ في حالة تعارض الموارد. يؤثر الترتيب أيضًا على كيفية تشكيل اسم Variant: تأتي flavors البعد الأول أولاً، ثم البعد الثاني، ثم Build Type: freeUsDebug.
عدد الأبعاد غير محدود، لكن كل بُعد جديد يضاعف عدد Build Variants. لمشروع بـ 4 أبعاد (flavors لكل منها) ونوعي build، تحصل على 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 هو الاسم والبُعد. جميع المعلمات الأخرى موروثة من defaultConfig ويمكن تجاوزها. يرث flavor defaultConfig بالكامل، بما في ذلك applicationId وversionCode وtestInstrumentationRunner.
يمكن لكل flavor تجاوز applicationId — وهذا يسمح بتثبيت إصدارات متعددة من التطبيق على نفس الجهاز في وقت واحد. على سبيل المثال، سيكون الإصدار المجاني com.example.app.free، والمدفوع — com.example.app.paid. إذا لم يتم تجاوز applicationId، سيكون لجميع الـ flavors نفس المعرف، ولا يمكن تثبيتها جنبًا إلى جنب. يجب أن يتطابق applicationId مع package في البيان (ما لم يتم استخدام applicationIdSuffix).
يوصي AGP 8+ باستخدام Kotlin DSL بدلاً من Groovy لـ build.gradle. يوفر 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 (تنفيذ فئة مجردة في كل flavor) وحقل BuildConfig (التفرع في الكود). الطريقة الأولى أكثر نظافة: تحدد واجهة أو فئة مجردة في main، وتنفيذات ملموسة في src/free/ و src/paid/. أثناء البناء، يتم ترجمة تنفيذ flavor الحالي فقط. هذا يوفر فوائد متزامنة: حجم APK أصغر (كود الدفع لا يدخل في الإصدار المجاني) والأمان (من المستحيل استدعاء وظيفة مدفوعة عن طريق الخطأ).
AndroidManifest.xml في source set الخاص بـ flavor لا يستبدل بل يندمج مع البيان الرئيسي. يتبع الدمج قواعد Android: يتم تجاوز السمات المكررة في نفس العنصر، وتضاف السمات الفريدة. على سبيل المثال، إذا أعلن البيان الرئيسي عن إذن INTERNET ولم يعلن free عنه، يبقى إذن الإنترنت. ومع ذلك، tools:node="replace" يسمح باستبدال كتلة بيان كاملة لـ flavor معين. هذا مفيد عندما تتطلب flavors مختلفة أذونات مختلفة (الكتابة على بطاقة SD للإصدار المدفوع، الكاميرا للإصدار المجاني).
<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 — بدون إعلانات، بوظائف موسعة. للإصدار المجاني، يتم تعيين applicationId إلى "com.example.app.free"، للمدفوع — "com.example.app.paid". يمكن تثبيت كلا الإصدارين على نفس الجهاز في وقت واحد، لأن applicationId هو المعرف الفريد للتطبيق في نظام Android.
من الناحية المعمارية، يتم بناء الفصل من خلال واجهة + تنفيذ flavor. في source set الرئيسي، يتم تعريف واجهة PaymentService. في src/free/، يوجد تنفيذ يعرض إعلانًا قبل الدفع عبر AdMob. في src/paid/ — تنفيذ ينتقل مباشرة إلى بوابة الدفع. لا يعرف الكود الذي يستخدم PaymentService أي تنفيذ تم تحميله — يتم حل هذا في وقت الترجمة. يضمن هذا النهج أن كود إدارة الاشتراكات لن يظهر في الإصدار المجاني، حتى إذا استدعاه المطور عن طريق الخطأ.
يمكن أن يختلف حجم APK لـ flavors مختلفة بمقدار 5-15 ميجابايت بسبب تضمين/استبعاد التبعيات. لاستبعاد مكتبة من flavor معين، استخدم تبعيات خاصة بـ flavor في build.gradle: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0'. ستتم إضافة هذه التبعية فقط للنسخة المجانية ولن تزيد من حجم الإصدار المدفوع. للتبعيات المشتركة، استخدم implementation — جميع الـ flavors تتضمنها.
// 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)، بينما تتوقع وحدة التطبيق مع flavor المكتبة بالمتغير المقابل. بدءًا من AGP 8.1، يمكن للمكتبات نشر متغيرات متعددة من خلال كتلة publishing.multipleVariants — وهذا يسمح بنشر جميع متغيرات flavor للمكتبة في مستودع maven واحد، وستحدد وحدة التطبيق تلقائيًا المتغير المناسب.
النهج البديل هو تعريف نفس flavorDimensions و productFlavors في المكتبة كما في وحدة التطبيق. يطابق AGP تلقائيًا الـ flavors عن طريق تطابق الاسم بالضبط ضمن بُعد واحد. إذا تطابق اسم flavor في المكتبة مع الاسم في التطبيق، سينشئ AGP متغيرات متسقة. لسهولة الصيانة، يوصى باستخراج تعريفات الـ flavors الشائعة في Convention Plugin — وهو مكون إضافي لـ Gradle يتم تطبيقه على جميع وحدات المشروع.
للمكتبات غير المخصصة للنشر (الوحدات الداخلية)، يكفي مزامنة الـ flavors من خلال build.gradle للمشروع الجذر. يوفر Gradle طريقة subprojects، التي تسمح بتطبيق التكوين على جميع المشاريع الفرعية. ومع ذلك، ضع في اعتبارك أن الكثير من التكوين في subprojects يبطئ مرحلة التكوين. يوصى باستخدام Convention Plugins — يتم ترجمتها مرة واحدة وإعادة استخدامها، مما يقلل وقت التكوين بنسبة 15-30%.
الأسئلة المتداولة
لا يوجد حد للكمية، لكن كل بُعد يضاعف عدد Build Variants. 4 flavors في بُعد واحد + 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'. سيتم تضمين هذه التبعية فقط عند بناء النسخة المجانية. للمدفوع: paidImplementation. يتم تحديد التبعيات المشتركة من خلال implementation.
Product Flavor يحدد إصدار المنتج (free, paid, demo)، Build Type يحدد طريقة البناء (debug, release). يمكن لـ الـ flavors تجاوز applicationId وversionName والموارد. يتحكم Build Type في debuggable وminification وsigning. كلاهما متعامدان ويتم دمجهما في Build Variant.
نعم، Product Flavors تعمل مع Compose دون قيود. يمكن أن يكون لـ الـ flavors المختلفة شاشات Compose مختلفة من خلال source sets أو تنفيذ فئات مجردة. يمكنك أيضًا إضافة تبعيات Compose خاصة بـ flavor: freeImplementation 'androidx.compose.ui:ui-tooling'.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا