产品风味入门:从“一道菜”到“菜单”的蜕变

各位同学,欢迎来到构建变体的第一站。今天我们来聊聊一个非常实用、也非常有意思的概念——产品风味(Product Flavors)

说白了,产品风味就是让你能用同一套代码,变出多个不同版本的App。比如免费版和付费版,或者国内版和国际版。我刚开始接触这个功能时,心里想的是:“这不就是复制粘贴改个包名吗?” 后来发现,天真了。产品风味远不止改个名字那么简单。

为什么需要产品风味?

想象一下,你开发了一款很棒的笔记应用。你想推出一个免费版(带广告、功能少一点),和一个付费版(无广告、功能全开)。

最笨的办法是什么?复制整个项目,改代码,维护两个工程。嗯,我早期带团队时真见过有人这么干。结果呢?修一个bug要改两遍,加一个功能要同步两次。后来代码越来越不一样,最后两个版本变成了两个完全不同的App,维护成本直接翻倍。

产品风味就是为了解决这个痛点。它让你在同一个工程里,通过配置和代码组织,生成多个不同风味的APK。核心代码共享,差异部分隔离。这才是工程化的正确姿势。

flavorDimensions:给风味分个类

光有风味还不够。如果你的App既要分免费/付费,又要分国内/国外,怎么办?这时候就需要风味维度(flavorDimensions)了。

你可以把flavorDimensions理解成“分类的标签”。比如:

  • 一个维度叫 version,包含 free 和 paid 两种风味。
  • 另一个维度叫 market,包含 china 和 global 两种风味。

这样组合起来,你就能生成 freeChinafreeGlobalpaidChinapaidGlobal 四种风味。是不是很灵活?

我个人习惯把最核心的差异放在第一个维度。比如付费模式放第一维,市场放第二维。这样在配置时思路更清晰。

实战:创建免费版和付费版

光说不练假把式。我们直接上手,在 build.gradle(Module级别)里配置一下。

先看一个最简单的例子:

android {
    ...
    flavorDimensions "version"

    productFlavors {
        free {
            dimension "version"
            applicationIdSuffix ".free"
            versionNameSuffix "-free"
        }
        paid {
            dimension "version"
            applicationIdSuffix ".paid"
            versionNameSuffix "-paid"
        }
    }
}

这里我做了几件事:

  • 定义了一个维度叫 version
  • 创建了两个风味:freepaid
  • 给每个风味加了不同的 applicationIdSuffix,这样它们可以同时安装在同一台手机上。
  • 加了 versionNameSuffix,方便在设置里区分版本。

配置完同步一下,你会在 Android Studio 的 Build Variants 面板里看到两个变体:freeDebugfreeReleasepaidDebugpaidRelease。选中一个,直接运行,就能生成对应风味的APK。

重要提醒: 配置完 productFlavors 后,一定要先 Sync Now。否则 AS 不会识别新的变体。

代码层面的差异处理

光改包名和版本号还不够。免费版和付费版的功能肯定不一样。怎么在代码里区分?

Android Gradle 插件提供了一种非常优雅的方式:风味特定的源集(flavor-specific source sets)

你可以在 src/ 目录下,创建与风味同名的文件夹。比如:

  • src/free/ — 存放免费版特有的代码和资源。
  • src/paid/ — 存放付费版特有的代码和资源。
  • src/main/ — 存放所有风味共享的代码和资源。

举个例子。免费版需要展示广告,付费版不需要。你可以这样做:

src/main/java/.../MainActivity.java 中:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 调用风味特定的方法
        showAdIfNeeded();
    }
}

然后在 src/free/java/.../MainActivity.java 中:

// 注意:这里不是新建一个类,而是扩展同一个类
// 实际上更常见的做法是使用 BuildConfig 或接口
// 这里为了演示,假设我们使用 flavor 特定的实现
public class MainActivity {
    private void showAdIfNeeded() {
        // 显示广告的逻辑
        Log.d("Ad", "免费版,显示广告");
    }
}

src/paid/java/.../MainActivity.java 中:

public class MainActivity {
    private void showAdIfNeeded() {
        // 付费版,什么都不做
        Log.d("Ad", "付费版,无广告");
    }
}

嗯,这里要注意:风味源集中的类不能与 main 源集中的类完全重名,否则会冲突。更常见的做法是使用 BuildConfig 字段或者依赖注入来区分。不过对于初学者,先理解这个“按风味覆盖”的思路就好。

用 BuildConfig 做运行时判断

除了源集覆盖,你还可以在代码里通过 BuildConfig 来判断当前是哪个风味。比如:

if (BuildConfig.FLAVOR.equals("free")) {
    // 免费版逻辑
} else if (BuildConfig.FLAVOR.equals("paid")) {
    // 付费版逻辑
}

这个方法简单直接,但我不建议在大量地方使用。为什么呢?因为一旦风味多了,代码里到处都是 if-else,维护起来很头疼。我曾经在一个项目里看到几百处 BuildConfig.FLAVOR 判断,改一个风味名字差点改出 bug。

更好的做法是:把差异封装成接口或抽象类,不同风味提供不同实现。这样代码更干净,也更容易测试。

知识结构图

下面我用一张 SVG 图来总结本章的核心逻辑。你可以把它当作一个“风味配置地图”。

产品风味知识结构 Product Flavors flavorDimensions 定义分类维度 productFlavors 创建具体风味 flavor源集 代码与资源隔离 示例:version维度 free(免费版) paid(付费版) 示例:market维度 china(国内版) global(国际版) 生成多种风味APK(如 freeChina、paidGlobal)

避坑指南

最后,分享几个我踩过的坑:

  • flavorDimensions 的顺序很重要。 第一个维度优先级最高。在组合风味时,第一个维度的值会出现在 APK 名称的前面。比如 freeChinachinaFree 是不同的。
  • 不要忘记在 productFlavors 里指定 dimension。 如果不指定,Gradle 会报错。我刚开始学的时候漏过一次,找了半天原因。
  • 源集覆盖是叠加的,不是替换的。 如果你在 main 里有一个类,在 free 里也有一个同名的类,编译会报错。正确的做法是用不同的类名,或者使用 BuildConfig 做判断。
小技巧: 你可以在 build.gradle 里为每个风味单独配置 resConfigs,只打包该风味需要的语言资源。这样能有效减小 APK 体积。比如国内版只保留中文资源,国际版保留英文资源。

好了,关于产品风味的基础知识就讲到这里。记住,产品风味的核心思想是:共享代码,隔离差异。掌握了这个思路,你就能轻松应对多版本发布的场景了。


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