产品风味进阶:多维度组合、风味变体过滤、变体源集(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会自动生成所有组合:chinaFreechinaProusaFreeusaPro。一共4个变体。你想想看,如果每个维度再加一个选项,组合数会爆炸式增长。所以这里有个小建议:维度不要超过3个,否则构建列表会变得非常长,我自己就吃过这个亏。

💡 我的经验: 维度顺序很重要!写在前面的是「主要维度」,生成的变体名称会优先按这个维度排序。我一般把变化最频繁的维度放前面,比如「tier」。

风味变体过滤:别让无用的变体浪费你的时间

维度多了以后,你会发现有些组合根本不需要。比如「中国区免费版」和「美国区免费版」可能逻辑完全不同,但「中国区专业版」压根不存在。这时候就需要过滤掉无用的变体。

怎么做?用 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)

调试一下,心里就有底了。

⚠️ 避坑指南: 我曾经在过滤条件里写错了维度名称,结果所有变体都被忽略了,构建直接报错「No variants found」。排查了半天才发现是拼写问题。所以建议在过滤前先打印所有变体名称确认一遍。

变体源集(src)的目录结构:代码和资源怎么放?

配置完风味,接下来就是放代码和资源了。Gradle的源集目录结构遵循一个简单规则:先风味组合,后构建类型

拿上面的 chinaFreeDebug 变体来说,它的源集查找顺序是:

  1. src/chinaFreeDebug/(完整变体名)
  2. src/chinaFree/(风味组合)
  3. src/china/(单个风味)
  4. src/free/(另一个单个风味)
  5. src/debug/(构建类型)
  6. 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

但这里有个容易忽略的点:资源合并是「文件级别」还是「资源项级别」? 答案是后者。也就是说,同一个文件里,只有冲突的项才会被覆盖,不冲突的项会合并在一起。

🎯 核心要点: 资源合并是「按资源项合并」,不是「按文件合并」。所以你可以放心地在不同风味中只覆盖需要改动的字符串,其他字符串会从 main 继承。

我记得有一次,团队里有个同事在 china 风味里复制了整个 strings.xml,结果漏掉了一个新加的字符串,导致中国区版本一直显示英文。排查了半天才发现是覆盖了整份文件。所以我的建议是:每个风味只放需要改动的资源项,不要复制粘贴整个文件

知识体系总览

为了让你更直观地理解整个流程,我画了一张图:

产品风味进阶知识体系 多维度组合 • flavorDimensions 定义 • 自动生成所有组合 • 维度顺序影响命名 • 建议不超过3个维度 变体过滤 • variantFilter 闭包 • 按维度名称判断 • setIgnore(true) 跳过 • 先打印确认再过滤 源集与资源合并 • 目录查找优先级 • 按资源项合并 • 高优先级覆盖低优先级 • 只放改动项,不复制 完整工作流程 ① 定义维度与风味 ② 过滤无用变体 ③ 组织源集目录 ④ 构建时自动合并资源,高优先级覆盖低优先级 ⑤ 生成最终APK/AAB

这张图把整个流程串起来了。从定义维度开始,到过滤、组织目录,最后资源合并生成产物。每一步都环环相扣。

实战中的小技巧

最后分享几个我在项目中常用的技巧:

  • 善用 buildConfigField:在风味配置里定义 buildConfigField,比在代码里写 if-else 判断风味名要优雅得多。比如 buildConfigField("boolean", "IS_FREE", "true"),然后在代码里直接引用 BuildConfig.IS_FREE
  • 资源覆盖要克制:能不覆盖就不覆盖。如果只是改个颜色值,用 res/values/colors.xml 覆盖一个 <color> 标签就够了,别把整个 colors.xml 搬过去。
  • 调试时多用 variantFilter:开发阶段只保留一两个变体,构建速度会快很多。我通常只保留 chinaFreeDebugusaFreeDebug,其他全过滤掉。
📌 一点补充: 如果你用 Kotlin DSL 配置 Gradle,语法略有不同,但逻辑完全一样。我个人更喜欢 Groovy 的简洁,不过 Kotlin DSL 的类型安全确实香。看团队习惯吧。

好了,这一章的内容就到这里。产品风味进阶其实不难,关键是理解维度的组合逻辑和资源的优先级规则。多动手配几次,你就能找到感觉。


公众号:蓝海资料掘金营,微信deep3321