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 หรือการสะท้อน
Build Type จัดการกระบวนการสร้าง (debug ที่มีการดีบัก, release ที่มีการเพิ่มประสิทธิภาพ) Product Flavor จัดการเนื้อหาของการสร้าง (free ที่ไม่มีฟีเจอร์เสียเงิน, paid ที่มี) Build Type คือการตั้งค่าโครงสร้างพื้นฐาน Product Flavor คือการตั้งค่าผลิตภัณฑ์ ทั้งสองแนวคิดตั้งฉากกัน: การสร้าง debug ของ flavor free แตกต่างจากการสร้าง release ของ flavor free เฉพาะในพารามิเตอร์การคอมไพล์ ไม่ใช่ฟังก์ชันการทำงาน ไม่สามารถใช้ 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 มิติ (อย่างละ 2 flavors) และ 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 ขั้นต่ำคือชื่อและมิติ พารามิเตอร์อื่นๆ ทั้งหมดสืบทอดจาก defaultConfig และสามารถแทนที่ได้ Flavor สืบทอด defaultConfig อย่างสมบูรณ์ รวมถึง applicationId, versionCode, testInstrumentationRunner
แต่ละ flavor สามารถแทนที่ applicationId — ซึ่งอนุญาตให้ติดตั้งหลายเวอร์ชันของแอปบนอุปกรณ์เดียวกันพร้อมกัน ตัวอย่างเช่น เวอร์ชันฟรีจะเป็น com.example.app.free เวอร์ชันเสียเงิน — com.example.app.paid หากไม่ได้แทนที่ applicationId ทุก flavor จะมีตัวระบุเดียวกัน และไม่สามารถติดตั้งเคียงข้างกันได้ applicationId ต้องตรงกับแพ็คเกจในไฟล์แถลงการณ์ (ยกเว้นเมื่อใช้ applicationIdSuffix)
AGP 8+ แนะนำให้ใช้ Kotlin DSL แทน Groovy สำหรับ build.gradle Kotlin DSL ให้การเข้าถึงการกำหนดค่าแบบ type-safe: 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 เฉพาะ ซึ่งมีประโยชน์เมื่อ flavor ต่างๆ ต้องการสิทธิ์ที่แตกต่างกัน (การเขียน 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 — ไม่มีโฆษณา มีฟังก์ชันเพิ่มเติม สำหรับเวอร์ชันฟรี ตั้งค่า applicationId เป็น "com.example.app.free" สำหรับเวอร์ชันเสียเงิน — "com.example.app.paid" ทั้งสองเวอร์ชันสามารถติดตั้งบนอุปกรณ์เดียวกันพร้อมกันได้ เนื่องจาก applicationId เป็นตัวระบุเฉพาะของแอปพลิเคชันในระบบ Android
ในเชิงสถาปัตยกรรม การแยกส่วนสร้างขึ้นผ่าน อินเทอร์เฟซ + การใช้งาน flavor ใน source set หลัก มีการประกาศอินเทอร์เฟซ PaymentService ใน src/free/ มีการใช้งานที่แสดงโฆษณาก่อนการชำระเงินผ่าน AdMob ใน src/paid/ — การใช้งานที่ดำเนินการไปยังเกตเวย์การชำระเงินโดยตรง โค้ดที่ใช้ PaymentService ไม่ทราบว่าการใช้งานใดถูกโหลด — สิ่งนี้ถูกกำหนดในเวลาคอมไพล์ แนวทางนี้รับประกันว่าโค้ดการจัดการการสมัครสมาชิกจะไม่เข้าไปในเวอร์ชันฟรี แม้ว่านักพัฒนาจะเรียกใช้โดยไม่ตั้งใจก็ตาม
ขนาด APK สำหรับ flavor ต่างๆ อาจแตกต่างกัน 5-15 MB เนื่องจากการรวม/แยกการพึ่งพา หากต้องการแยกไลบรารีออกจาก flavor เฉพาะ ให้ใช้ การพึ่งพาเฉพาะ flavor ใน build.gradle: freeImplementation 'com.google.android.gms:play-services-ads:23.0.0' การพึ่งพานี้จะถูกเพิ่มสำหรับรูปแบบฟรีเท่านั้นและจะไม่เพิ่มขนาดของเวอร์ชันเสียเงิน สำหรับการพึ่งพาที่ใช้ร่วมกัน ให้ใช้ 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) ในขณะที่โมดูลแอปที่มี flavor คาดหวังให้ไลบรารีมีรูปแบบที่สอดคล้องกัน เริ่มตั้งแต่ AGP 8.1 ไลบรารีสามารถเผยแพร่ หลายรูปแบบ ผ่านบล็อก publishing.multipleVariants — ซึ่งอนุญาตให้เผยแพร่ทุกรูปแบบ flavor ของไลบรารีไปยังที่เก็บ maven เดียว และโมดูลแอปจะเลือกรูปแบบที่ถูกต้องโดยอัตโนมัติ
แนวทางอื่นคือการประกาศ flavorDimensions และ productFlavors เดียวกันในไลบรารีเช่นเดียวกับในโมดูลแอป AGP จับคู่ flavors โดยอัตโนมัติโดยการจับคู่ชื่อที่แน่นอนภายในหนึ่งมิติ หากชื่อ flavor ในไลบรารีตรงกับชื่อในแอป AGP จะสร้างรูปแบบที่สอดคล้องกัน เพื่อความสะดวกในการบำรุงรักษา ขอแนะนำให้แยกคำจำกัดความ flavor ทั่วไปไปไว้ใน 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 ได้โดยไม่มีข้อจำกัด flavor ต่างๆ สามารถมี หน้าจอ Compose ต่างกันผ่าน source sets หรือการนำคลาสนามธรรมไปใช้ คุณยังสามารถเพิ่มการพึ่งพา Compose เฉพาะ flavor: freeImplementation 'androidx.compose.ui:ui-tooling'
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม