21、构建变体高级用法:变体维度矩阵、变体专属源集、变体专属依赖、变体专属资源

各位同学,欢迎来到第二十一讲。前面我们聊了构建变体的基础,比如怎么定义 debugrelease。但说实话,真实项目里往往不止这么简单。你想想看,一个 App 可能要同时适配国内版、海外版、免费版、付费版,还要区分开发、测试、预发布环境。这时候,光靠两三个变体根本不够用。

嗯,这一讲我们就来啃硬骨头——变体维度矩阵。说白了,就是让多个维度组合起来,形成一套完整的变体矩阵。我还会带你看看变体专属的源集、依赖和资源怎么配置。这些都是我平时做项目时反复用到的技巧,希望能帮你少走弯路。

21.1 变体维度矩阵:从一维到多维

先问一个问题:如果你有 2 个 flavor(比如 demofull),再加上 2 个 buildType(debugrelease),最终会生成几个变体?答案是 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 个变体。比如 chinaFreeDebugoverseasProRelease 等等。

我的习惯:我会把最常变化的维度放在前面。比如市场维度经常需要新增地区,我就把它放在第一个维度。这样在 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 提供了 variantNameImplementationvariantNameApi 等配置方式。来看一个例子:

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 知识体系总览

为了让你更直观地理解这一章的内容,我画了一张结构图。它展示了变体维度、源集、依赖和资源之间的关系:

构建变体高级用法:知识体系 构建变体 变体维度矩阵 变体专属源集 变体专属依赖 变体专属资源 flavorDimensions 笛卡尔积组合 variantFilter 优先级查找 目录覆盖规则 自定义 sourceSets variantNameImpl flavor 组合依赖 版本隔离 图片/字符串 Manifest 合并 文件级覆盖 灵活组合,按需配置,避免冗余

从这张图你可以看到,四个分支各自独立又相互配合。维度矩阵决定了变体的数量,源集决定了代码和资源的组织方式,依赖和资源则让每个变体拥有自己的“个性”。

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 这样的命名,避免覆盖整个文件。

好了,这一讲的内容就到这里。变体高级用法其实不难,关键是理解它的设计思想——组合优于继承,配置胜于硬编码。希望你在实际项目中能灵活运用这些技巧。


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