产品风味进阶:多维度组合、风味变体过滤、变体源集(src)的目录结构、资源合并与覆盖规则
好,咱们接着聊产品风味。上一章我们讲了基础的风味配置,说白了就是给同一个App穿不同的马甲。但实际项目中,你会发现需求远不止这么简单——比如你的App要同时区分「国内版/海外版」和「免费版/付费版」,这就涉及到多维度组合了。
我个人习惯把产品风味想象成「矩阵」。每个维度是一条轴,交叉点就是一个具体的变体。听起来有点抽象?咱们直接上代码。
多维度组合:flavorDimensions 的妙用
先看一个典型场景:你的App需要同时支持「中国区」和「美国区」,每个区域又分「免费版」和「专业版」。这时候就需要定义两个维度:
android {
flavorDimensions "region", "tier"
productFlavors {
china {
dimension "region"
versionNameSuffix "-cn"
}
usa {
dimension "region"
versionNameSuffix "-us"
}
free {
dimension "tier"
versionNameSuffix "-free"
}
pro {
dimension "tier"
versionNameSuffix "-pro"
}
}
}
配置完以后,Gradle会自动生成所有组合:chinaFree、chinaPro、usaFree、usaPro。一共4个变体。你想想看,如果每个维度再加一个选项,组合数会爆炸式增长。所以这里有个小建议:维度不要超过3个,否则构建列表会变得非常长,我自己就吃过这个亏。
风味变体过滤:别让无用的变体浪费你的时间
维度多了以后,你会发现有些组合根本不需要。比如「中国区免费版」和「美国区免费版」可能逻辑完全不同,但「中国区专业版」压根不存在。这时候就需要过滤掉无用的变体。
怎么做?用 variantFilter 闭包:
android {
variantFilter { variant ->
def names = variant.flavors*.name
// 如果是中国区且是专业版,直接跳过
if (names.contains("china") && names.contains("pro")) {
setIgnore(true)
}
}
}
嗯,这里要注意:variant.flavors 返回的是一个列表,里面按维度顺序排列。我刚开始用的时候老记不住顺序,后来干脆打印出来看:
println("Flavors: " + variant.flavors*.name)
调试一下,心里就有底了。
变体源集(src)的目录结构:代码和资源怎么放?
配置完风味,接下来就是放代码和资源了。Gradle的源集目录结构遵循一个简单规则:先风味组合,后构建类型。
拿上面的 chinaFreeDebug 变体来说,它的源集查找顺序是:
src/chinaFreeDebug/(完整变体名)src/chinaFree/(风味组合)src/china/(单个风味)src/free/(另一个单个风味)src/debug/(构建类型)src/main/(公共源集)
说白了,Gradle会从最具体的目录开始找,找不到就往上一层。这个优先级顺序非常重要,因为后面讲资源合并时会用到。
实际项目中,我通常这样组织目录:
app/src/
├── main/
│ ├── java/
│ ├── res/
│ └── AndroidManifest.xml
├── china/
│ ├── res/
│ │ └── values/
│ │ └── strings.xml (中文文案)
│ └── java/
│ └── ... (中国区特有逻辑)
├── usa/
│ ├── res/
│ │ └── values/
│ │ └── strings.xml (英文文案)
│ └── java/
│ └── ... (美国区特有逻辑)
├── free/
│ └── res/
│ └── values/
│ └── bools.xml (isFree = true)
├── pro/
│ └── res/
│ └── values/
│ └── bools.xml (isFree = false)
└── debug/
└── res/
└── values/
└── strings.xml (调试用文案)
你看,每个风味只放自己特有的东西,公共部分全在 main 里。这样维护起来非常清晰。
资源合并与覆盖规则:谁说了算?
资源合并是很多新手容易踩坑的地方。简单来说,规则只有一条:优先级高的源集会覆盖优先级低的源集。优先级顺序就是上面说的查找顺序。
举个例子,假设 main/res/values/strings.xml 里定义了:
<string name="app_name">MyApp</string>
而 china/res/values/strings.xml 里定义了:
<string name="app_name">我的应用</string>
那么构建 chinaFreeDebug 时,最终 app_name 的值会是「我的应用」。因为 china 的优先级高于 main。
但这里有个容易忽略的点:资源合并是「文件级别」还是「资源项级别」? 答案是后者。也就是说,同一个文件里,只有冲突的项才会被覆盖,不冲突的项会合并在一起。
我记得有一次,团队里有个同事在 china 风味里复制了整个 strings.xml,结果漏掉了一个新加的字符串,导致中国区版本一直显示英文。排查了半天才发现是覆盖了整份文件。所以我的建议是:每个风味只放需要改动的资源项,不要复制粘贴整个文件。
知识体系总览
为了让你更直观地理解整个流程,我画了一张图:
这张图把整个流程串起来了。从定义维度开始,到过滤、组织目录,最后资源合并生成产物。每一步都环环相扣。
实战中的小技巧
最后分享几个我在项目中常用的技巧:
- 善用
buildConfigField:在风味配置里定义buildConfigField,比在代码里写 if-else 判断风味名要优雅得多。比如buildConfigField("boolean", "IS_FREE", "true"),然后在代码里直接引用BuildConfig.IS_FREE。 - 资源覆盖要克制:能不覆盖就不覆盖。如果只是改个颜色值,用
res/values/colors.xml覆盖一个<color>标签就够了,别把整个colors.xml搬过去。 - 调试时多用
variantFilter:开发阶段只保留一两个变体,构建速度会快很多。我通常只保留chinaFreeDebug和usaFreeDebug,其他全过滤掉。
好了,这一章的内容就到这里。产品风味进阶其实不难,关键是理解维度的组合逻辑和资源的优先级规则。多动手配几次,你就能找到感觉。