产品风味入门:从“一道菜”到“菜单”的蜕变
各位同学,欢迎来到构建变体的第一站。今天我们来聊聊一个非常实用、也非常有意思的概念——产品风味(Product Flavors)。
说白了,产品风味就是让你能用同一套代码,变出多个不同版本的App。比如免费版和付费版,或者国内版和国际版。我刚开始接触这个功能时,心里想的是:“这不就是复制粘贴改个包名吗?” 后来发现,天真了。产品风味远不止改个名字那么简单。
为什么需要产品风味?
想象一下,你开发了一款很棒的笔记应用。你想推出一个免费版(带广告、功能少一点),和一个付费版(无广告、功能全开)。
最笨的办法是什么?复制整个项目,改代码,维护两个工程。嗯,我早期带团队时真见过有人这么干。结果呢?修一个bug要改两遍,加一个功能要同步两次。后来代码越来越不一样,最后两个版本变成了两个完全不同的App,维护成本直接翻倍。
产品风味就是为了解决这个痛点。它让你在同一个工程里,通过配置和代码组织,生成多个不同风味的APK。核心代码共享,差异部分隔离。这才是工程化的正确姿势。
flavorDimensions:给风味分个类
光有风味还不够。如果你的App既要分免费/付费,又要分国内/国外,怎么办?这时候就需要风味维度(flavorDimensions)了。
你可以把flavorDimensions理解成“分类的标签”。比如:
- 一个维度叫
version,包含 free 和 paid 两种风味。 - 另一个维度叫
market,包含 china 和 global 两种风味。
这样组合起来,你就能生成 freeChina、freeGlobal、paidChina、paidGlobal 四种风味。是不是很灵活?
我个人习惯把最核心的差异放在第一个维度。比如付费模式放第一维,市场放第二维。这样在配置时思路更清晰。
实战:创建免费版和付费版
光说不练假把式。我们直接上手,在 build.gradle(Module级别)里配置一下。
先看一个最简单的例子:
android {
...
flavorDimensions "version"
productFlavors {
free {
dimension "version"
applicationIdSuffix ".free"
versionNameSuffix "-free"
}
paid {
dimension "version"
applicationIdSuffix ".paid"
versionNameSuffix "-paid"
}
}
}
这里我做了几件事:
- 定义了一个维度叫
version。 - 创建了两个风味:
free和paid。 - 给每个风味加了不同的
applicationIdSuffix,这样它们可以同时安装在同一台手机上。 - 加了
versionNameSuffix,方便在设置里区分版本。
配置完同步一下,你会在 Android Studio 的 Build Variants 面板里看到两个变体:freeDebug、freeRelease、paidDebug、paidRelease。选中一个,直接运行,就能生成对应风味的APK。
代码层面的差异处理
光改包名和版本号还不够。免费版和付费版的功能肯定不一样。怎么在代码里区分?
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 图来总结本章的核心逻辑。你可以把它当作一个“风味配置地图”。
避坑指南
最后,分享几个我踩过的坑:
- flavorDimensions 的顺序很重要。 第一个维度优先级最高。在组合风味时,第一个维度的值会出现在 APK 名称的前面。比如
freeChina和chinaFree是不同的。 - 不要忘记在 productFlavors 里指定 dimension。 如果不指定,Gradle 会报错。我刚开始学的时候漏过一次,找了半天原因。
- 源集覆盖是叠加的,不是替换的。 如果你在 main 里有一个类,在 free 里也有一个同名的类,编译会报错。正确的做法是用不同的类名,或者使用 BuildConfig 做判断。
build.gradle 里为每个风味单独配置 resConfigs,只打包该风味需要的语言资源。这样能有效减小 APK 体积。比如国内版只保留中文资源,国际版保留英文资源。
好了,关于产品风味的基础知识就讲到这里。记住,产品风味的核心思想是:共享代码,隔离差异。掌握了这个思路,你就能轻松应对多版本发布的场景了。
公众号:蓝海资料掘金营,微信deep3321