12、构建变体与风味:Android构建变体、SourceSet配置、产品风味
说到Android构建,我猜你肯定遇到过这种情况:
「测试版要连测试服务器,正式版要连生产服务器。」
「免费版功能少一些,付费版功能全开。」
「国内版用微信登录,海外版用Google登录。」
这些需求,说白了就是构建变体和产品风味要解决的问题。Gradle Kotlin DSL 在这块做得特别顺手,类型安全让配置变得很舒服。
12.1 构建变体:Debug vs Release
构建变体(Build Variant)是 Android 构建的基础概念。每个变体 = 构建类型 × 产品风味。
构建类型最常见的就是 debug 和 release。我个人的习惯是,debug 包打开日志、不混淆、签名用 debug keystore;release 包则反过来。
// build.gradle.kts
android {
buildTypes {
debug {
isMinifyEnabled = false
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
这里有个细节:applicationIdSuffix 加上后,debug 和 release 包可以同时安装在同一台手机上。我在项目中经常用这个特性,一边测新功能,一边保留旧版本做对比。
12.2 产品风味:免费版 vs 付费版
产品风味(Product Flavor)用来区分不同版本的 App。比如免费版、付费版、国内版、海外版。
android {
flavorDimensions += "version"
productFlavors {
create("free") {
dimension = "version"
applicationIdSuffix = ".free"
versionNameSuffix = "-free"
}
create("paid") {
dimension = "version"
applicationIdSuffix = ".paid"
versionNameSuffix = "-paid"
}
}
}
你看,用 Kotlin DSL 的 create 函数创建风味,比 Groovy 的字符串配置清晰多了。每个风味都是一个类型安全的对象,可以配置 applicationId、versionName、minSdk 等等。
为什么会需要多个风味维度?举个例子:
- 维度一:version(free / paid)
- 维度二:market(china / global)
这样组合出来就是 2×2=4 个变体:freeChina、freeGlobal、paidChina、paidGlobal。
12.3 SourceSet 配置:代码与资源分离
SourceSet 是 Gradle 里管理源码和资源的概念。Android 插件默认有 main source set,所有风味和构建类型可以有自己的 source set。
目录结构大概是这样的:
app/
├── src/
│ ├── main/ # 公共代码和资源
│ │ ├── java/
│ │ ├── res/
│ │ └── AndroidManifest.xml
│ ├── free/ # free 风味专属
│ │ ├── java/
│ │ └── res/
│ ├── paid/ # paid 风味专属
│ │ ├── java/
│ │ └── res/
│ ├── debug/ # debug 构建类型专属
│ │ └── res/
│ └── release/ # release 构建类型专属
│ └── res/
我记得有一次,项目需要免费版和付费版有不同的启动页。我就在 free 和 paid 目录下各放了一个 splash_screen.xml,main 目录里不放。Gradle 会自动合并,优先使用风味目录下的资源。
在 Kotlin DSL 里,你可以这样配置 source set:
android {
sourceSets {
getByName("main") {
java.srcDirs("src/main/java")
res.srcDirs("src/main/res")
}
getByName("free") {
res.srcDirs("src/free/res")
}
getByName("paid") {
res.srcDirs("src/paid/res")
}
}
}
嗯,这里要注意:getByName 是 Kotlin DSL 的写法,如果你写错了名字,编译时会直接报错。比 Groovy 的字符串配置安全很多。
12.4 构建变体与 SourceSet 的合并规则
Gradle 在构建时,会把 main、风味、构建类型的 source set 按优先级合并。优先级从高到低是:
- 构建类型 source set(如 debug、release)
- 产品风味 source set(如 free、paid)
- main source set
也就是说,如果同一个文件在多个 source set 里出现,高优先级的会覆盖低优先级的。但 AndroidManifest.xml 是合并的,不是覆盖。
strings.xml,里面只写了 app_name。结果 main 里的其他字符串资源全没了。后来才明白,资源文件是按目录合并的,不是按文件覆盖。所以风味目录里最好只放需要覆盖的资源,不要放完整的文件。
12.5 实战:用 Kotlin DSL 配置多风味构建
下面是一个完整的例子,展示如何用 Kotlin DSL 配置构建变体和风味:
android {
namespace = "com.example.myapp"
compileSdk = 34
flavorDimensions += listOf("version", "market")
productFlavors {
create("free") {
dimension = "version"
applicationIdSuffix = ".free"
versionNameSuffix = "-free"
buildConfigField("boolean", "IS_PREMIUM", "false")
}
create("paid") {
dimension = "version"
applicationIdSuffix = ".paid"
versionNameSuffix = "-paid"
buildConfigField("boolean", "IS_PREMIUM", "true")
}
create("china") {
dimension = "market"
versionNameSuffix = "-cn"
}
create("global") {
dimension = "market"
versionNameSuffix = "-global"
}
}
buildTypes {
debug {
isMinifyEnabled = false
buildConfigField("String", "API_BASE_URL", "\"https://test.api.com\"")
}
release {
isMinifyEnabled = true
buildConfigField("String", "API_BASE_URL", "\"https://api.com\"")
}
}
}
这样配置后,你会得到 2×2×2=8 个构建变体。每个变体都可以有不同的代码、资源、配置。
我个人习惯用 buildConfigField 来注入环境变量。比如 API 地址、功能开关等。这样代码里直接引用 BuildConfig.API_BASE_URL 就行,不用写 if-else 判断。
12.6 知识体系图
下面这张图帮你理清构建变体、产品风味、SourceSet 之间的关系:
从图中可以清楚看到:构建类型和产品风味组合成构建变体,每个变体对应一组 SourceSet。Gradle 会按照优先级合并这些 SourceSet 中的代码和资源。
12.7 避坑指南
最后分享几个我踩过的坑:
- 资源覆盖问题: 风味目录里不要放完整的资源文件,只放需要覆盖的部分。否则 main 里的资源会被整个替换掉。
- BuildConfig 字段: 不同风味和构建类型可以定义不同的 BuildConfig 字段,但要注意类型匹配。String 类型要加双引号转义。
- 依赖管理: 可以用
freeImplementation、paidImplementation来为特定风味添加依赖。比如免费版用广告 SDK,付费版不用。 - 签名配置: release 构建类型一定要配置签名信息,否则打出来的包没法发布。debug 构建类型会自动用 debug keystore。
构建变体和产品风味是 Android 多版本管理的核心。用 Kotlin DSL 配置起来,类型安全、代码清晰,比 Groovy 舒服太多了。你只要记住:变体 = 类型 × 风味,SourceSet 按优先级合并,就掌握了精髓。
公众号:蓝海资料掘金营,微信deep3321