21、构建变体高级用法:变体维度矩阵、变体专属源集、变体专属依赖、变体专属资源
各位同学,欢迎来到第二十一讲。前面我们聊了构建变体的基础,比如怎么定义 debug 和 release。但说实话,真实项目里往往不止这么简单。你想想看,一个 App 可能要同时适配国内版、海外版、免费版、付费版,还要区分开发、测试、预发布环境。这时候,光靠两三个变体根本不够用。
嗯,这一讲我们就来啃硬骨头——变体维度矩阵。说白了,就是让多个维度组合起来,形成一套完整的变体矩阵。我还会带你看看变体专属的源集、依赖和资源怎么配置。这些都是我平时做项目时反复用到的技巧,希望能帮你少走弯路。
21.1 变体维度矩阵:从一维到多维
先问一个问题:如果你有 2 个 flavor(比如 demo 和 full),再加上 2 个 buildType(debug 和 release),最终会生成几个变体?答案是 4 个。这就是一维组合。
但现实往往更复杂。比如你的 App 要同时区分 国内/海外 和 免费/付费,这就变成了两个 flavor 维度。再加上 buildType,就是三维矩阵了。
核心概念:变体维度(flavorDimensions)允许你定义多个分类轴,每个轴上的 flavor 会与其他轴上的 flavor 做笛卡尔积,生成所有可能的变体组合。
来看一个实际例子。我在做一个国际化项目时,需要同时管理市场(market)和版本(version)两个维度:
android {
flavorDimensions "market", "version"
productFlavors {
// 市场维度
china {
dimension "market"
}
overseas {
dimension "market"
}
// 版本维度
free {
dimension "version"
}
pro {
dimension "version"
}
}
buildTypes {
debug {}
release {}
}
}
这段配置会生成多少变体?我们来算一下:2 个 market × 2 个 version × 2 个 buildType = 8 个变体。比如 chinaFreeDebug、overseasProRelease 等等。
我的习惯:我会把最常变化的维度放在前面。比如市场维度经常需要新增地区,我就把它放在第一个维度。这样在 Gradle 任务命名时,变体名称会更直观。
这里有个坑要注意:变体名称的生成顺序。默认情况下,变体名称是 flavor1Flavor2BuildType 这样的格式。如果你觉得不好读,可以用 variantFilter 来过滤或重命名。我曾经因为变体名称太长,导致 CI 脚本解析出错,后来统一用短名称才解决。
21.2 变体专属源集:代码和资源的分而治之
有了变体矩阵,接下来就是怎么让每个变体拥有自己的代码和资源。这就是 变体专属源集(sourceSets) 的用武之地。
Gradle 的源集机制非常灵活。默认情况下,每个变体都会自动对应一个源集目录。比如 chinaFreeDebug 这个变体,它会按优先级从高到低查找以下目录:
src/chinaFreeDebug/(变体专属)src/chinaFree/(flavor 组合)src/china/(单个 flavor)src/free/(单个 flavor)src/debug/(buildType)src/main/(公共源集)
这个查找顺序很重要。高优先级的目录会覆盖低优先级目录中的同名文件。我见过不少新手把资源放错目录,结果某个变体死活找不到图片,其实就是优先级搞反了。
避坑指南:我曾经在 src/china/ 和 src/free/ 里都放了 strings.xml,结果 chinaFree 变体只读取了 src/china/ 里的字符串,因为 flavor 组合的优先级高于单个 flavor。后来我统一把公共字符串放到 src/main/,只在专属目录放差异化的内容。
如果你想自定义源集路径,也可以手动配置:
android {
sourceSets {
chinaFreeDebug {
java.srcDirs = ['src/custom_java']
res.srcDirs = ['src/custom_res']
}
}
}
不过我个人建议尽量遵循默认目录结构,除非有特殊需求。因为默认结构已经足够清晰,团队成员也容易理解。
21.3 变体专属依赖:按需引入,避免臃肿
不同变体可能需要不同的依赖。比如免费版用广告 SDK,付费版不用;国内版用微信支付,海外版用 Stripe。这时候就需要 变体专属依赖。
Gradle 提供了 variantNameImplementation、variantNameApi 等配置方式。来看一个例子:
dependencies {
// 公共依赖
implementation 'androidx.appcompat:appcompat:1.6.1'
// 国内版专属
chinaImplementation 'com.tencent.mm.opensdk:wechat-sdk-android:6.8.0'
// 海外版专属
overseasImplementation 'com.stripe:stripe-android:20.0.0'
// 免费版专属
freeImplementation 'com.google.android.gms:play-services-ads:22.0.0'
// debug 专属
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}
注意这里的命名规则:flavor1Flavor2BuildType + 配置名称。比如 chinaFreeDebugImplementation 就是只对 chinaFreeDebug 变体生效。
小技巧:如果你有多个变体共享同一个依赖,可以用 flavor1 + flavor2 + 配置名称 的方式。比如 chinaFreeImplementation 会对所有 chinaFree 开头的变体生效,不管 buildType 是什么。
我在项目中遇到过一个问题:免费版和付费版都用了同一个库,但版本不同。这时候如果直接写 implementation,两个变体会冲突。解决方案就是用变体专属依赖,各自指定版本:
freeImplementation 'com.example:library:1.0.0'
proImplementation 'com.example:library:2.0.0'
这样打包时,每个变体只会引入自己需要的版本,互不干扰。
21.4 变体专属资源:图片、字符串、颜色随心配
资源管理是变体配置中最直观的部分。不同变体可以拥有不同的图标、启动页、字符串甚至颜色主题。实现方式很简单:在对应的源集目录下放同名资源文件即可。
比如,国内版和海外版的 App 名称不同:
src/china/res/values/strings.xml里写<string name="app_name">我的应用</string>src/overseas/res/values/strings.xml里写<string name="app_name">My App</string>
再比如,免费版和付费版的图标不同:
src/free/res/mipmap-xxxhdpi/ic_launcher.png放免费版图标src/pro/res/mipmap-xxxhdpi/ic_launcher.png放付费版图标
这里有个细节:资源覆盖是文件级别的。也就是说,如果你在 src/china/ 里放了一个 ic_launcher.png,它会完全覆盖 src/main/ 里的同名文件。但如果你只改了 strings.xml 里的一个字符串,其他字符串仍然来自 src/main/。
我的建议:尽量把公共资源放在 src/main/,只在专属目录放差异化的资源。这样维护起来更轻松,也避免重复。
除了图片和字符串,你还可以配置 AndroidManifest 合并。比如国内版需要声明 READ_PHONE_STATE 权限,海外版不需要。你可以在 src/china/AndroidManifest.xml 里添加这个权限,Gradle 会自动合并到最终的 Manifest 中。
21.5 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张结构图。它展示了变体维度、源集、依赖和资源之间的关系:
从这张图你可以看到,四个分支各自独立又相互配合。维度矩阵决定了变体的数量,源集决定了代码和资源的组织方式,依赖和资源则让每个变体拥有自己的“个性”。
21.6 实战中的一些思考
最后,我想分享几个实战中的经验。
第一,不要滥用维度。 我见过有人把 minSdk 也做成一个维度,结果变体数量爆炸,编译时间翻倍。一般来说,2-3 个维度就足够了。如果超过 3 个,建议重新审视你的产品架构。
第二,善用 variantFilter 减少无用变体。 比如 chinaProDebug 这个变体可能永远用不到,直接过滤掉:
android.variantFilter { variant ->
if (variant.name == "chinaProDebug") {
setIgnore(true)
}
}
第三,资源覆盖要谨慎。 我曾经在 src/china/ 里放了一个 colors.xml,只改了主色调,结果其他颜色因为文件被覆盖而丢失。后来我改用 values/china_colors.xml 这样的命名,避免覆盖整个文件。
好了,这一讲的内容就到这里。变体高级用法其实不难,关键是理解它的设计思想——组合优于继承,配置胜于硬编码。希望你在实际项目中能灵活运用这些技巧。