Android Gradle flavor —— 打造不同风味的app
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gradle flavor —— 打造不同风味的app相关的知识,希望对你有一定的参考价值。
好文推荐:
作者:knight康康
何时会用的 flavor ?
一个产品,主体功能相同,但又存在差异。比如国内各个手机厂商众多。你的产品想使用各个厂商手机内置的钱包支付功能怎么办?
把所以厂商sdk集成,然后根据厂商品牌做条件判断,好像也是一种办法。缺点就是用户只有一种支付方式,却要把所以厂商钱包都集成进去🙄。 解决这样类似的需求,该 flavor 出场了。
flavor基础配置
android{
...
//定义风味维度
flavorDimensions "channel"
//声明产品风味
productFlavors {
//声明风味
huawei {
//指定风味维度
dimension "channel"
}
xiaomi {
dimension "channel"
}
...
}
}
flavorDimensions 维度可以指定多个,具体如何使用下文分解。
简单使用
不同风味的区别
同步一下工程,你就能在 Build Variants 工具栏看到配置的相关信息。
我们选中不同的 Build Variant 就可以运行不同风味的 app了。目前两者风味的 app 并没有什么区别,唯一的区别是 BuildConfig 的 FLAVOR 值不同,选择不同的风味,BuildConfig 的 FLAVOR 值就是对应风味的名称。
// 选择huaweiDebug
public final class BuildConfig {
...
public static final String FLAVOR = "huawei";
}
// 选择xiaomiDebug
public final class BuildConfig {
...
public static final String FLAVOR = "xiaomi";
}
打包不同风味的 apk
可以通过 Gradle 工具栏来选择性的进行打包
# 打所有风味的包
assemble
# 只打华为风味的包
assembleHuawei
# 只打小米风味的包
assembleXiaomi
来个需求场景实战
需求描述:接入不同应用商店的联运服务,发布到对应应用商店具有对应的联运功能。
联运:就是有钱一起挣,应用商店帮你推广,你使用厂商的支付,赚到的钱,他们要抽成。如 华为联运服务
实现方案一
引入相关依赖
implementation "华为联运sdk xxxx"
implementation "联运sdk xxxx"
通过 BuildConfig.FLAVOR
进行判断
/**
* 支付帮助类
*/
class PayHelper {
fun pay() {
when (BuildConfig.FLAVOR) {
"huawei" -> huaweiPay()
"xiaomi" -> xiaomiPay()
}
}
private fun huaweiPay() {
//通过使用华为联运sdk提供的api实现支付共功能
}
private fun xiaomiPay() {
//通过使用小米联运sdk提供的api实现支付共功能
}
}
这个方案看着是可以实现需求的。但存在一下缺点:
1.代码耦合度高。各种风味需求通过加判断区分,如果还有其它风味还要继续加判断,还有可能出现其它需要,那么还要这样加判断来满足不同风味的需求。
2. apk增大,apk包存在无用的代码。各个风味代码只是通过加判断区分,打包的时候代码还是会在apk中。
3. 重点是可能会审核不通过,针对联运这个需求,华为应用商店有要求,接入联运服务的app,不得加入第三方支付,正能使用华为钱包支付。如果按上面思路去写,apk包含了第三方支付的sdk,那么审核都过不了。
实现方案二
当我们指定不同风味的时候,gradle 构建成功后,会帮我们创建对应风味添加依赖的方法,这种依赖方式只有对应风味的才会引入对应的依赖。
生成的规则是:风味名+依赖方式名
所以上面引入联运sdk方式改成下面的写法。
//只有 huawei 风味变种下才会依赖此sdk
huaweiImplementation "华为联运sdk xxxx"
//只有 xiaomi 风味变种下才会依赖此sdk
xiaomiImplementation "联运sdk xxxx"
通过这种方式就可以隔离各个风味下的需要的SDK ,打包的时候就不会混在一起了。
如何编写对应风味的代码
当我们声明风味的时候,我们可以将代码写到对应风味目录中。默认情况我们代码都是写在app/src/main/ 这个目录下的。这个是默认路径我们也可以通过配置 sourceSets
自定义路径。
当我们配置产品风味时,我们的风味代码默认可以写在 app/src/风味名/ 。我们根据这个默认的规则创建对应的目录编写对应的代码,也可以通过 sourceSets
自定义路径。
huawei/
目录下的结构 和 main/
目录结构时一样的
上面 huiawei 目录下的 java 图标颜色和 main 目录下的是一样的,但xiaomi 目录下的 java 是灰色的图标,这是因为我们的 buildVariants 选这的是 huawei 风味的,此时只会构建对应风味的代码,此时在huawei目录下写代码时有提示的,但在小米目录下就没有提示,所以当我们写对应风味代码的时候,也要将 buildVariants 切到对应的 风味上去(如果你写代码不需要编辑器提示的可以忽略 😛 )
通过上面的配置我们就可以在对应风味下面写具体代码了。
app/src/huawei/java/com/wkk/flavordemo/PayHelper.kt
package com.wkk.flavordemo
import android.util.Log
/**
* 支付帮助类
*/
private const val TAG = "PayHelper"
object PayHelper {
fun pay() {
Log.i(TAG, "pay: 通过使用华为联运sdk提供的api实现支付共功能")
}
}
app/src/xiaomi/java/com/wkk/flavordemo/PayHelper.kt
package com.wkk.flavordemo
import android.util.Log
/**
* 支付帮助类
*/
private const val TAG = "PayHelper"
object PayHelper {
fun pay() {
Log.i(TAG, "pay: 通过使用小米联运sdk提供的api实现支付共功能")
}
}
在两个风味目录下,写相同类名相同方法名,但各自的方法实现不同,当我切换不同风味的 Build Variants 时,就会使用对应风味的方法实现。各个风味的代码实现分散到各个风味目录下,互不影响。
这种写法有几点需要注意:
风味目录中的类名不能和 main 目录下的类名不能有相同的,否则会有冲突。我们打包的时候,风味代码和资源会和 main 中的代码资源进行合并,类名如果出现同名就会报错,但资源文件文件重名或id重名是可以的,风味目录资源会覆盖main中的资源。
说到代码合并冲突的问题,就要说一说风味维度的东西了。
flavor 多维度
定义多纬度
//定义风味维度
flavorDimensions "channel","type"/*再声明一种维度*/
//声明产品风味
productFlavors {
//声明风味
huawei {
//指定风味维度
dimension "channel"
}
xiaomi {
dimension "channel"
}
//声明风味,但此风味维度为 type
free{
dimension "type"
}
}
我们可以在 flavorDimensions 后面定义许多维度,维度的定义是有顺序区别的。现在我们又声明一种风味 free
,但这个风味指定的维度和 huawei/xiaomi 风味不同。
此时的 build variant 就变成了: 维度为 channel 的风味 + 维度为 type 的风味 + buildType。根据 build variants 可以看出 相同纬度的风味不会组合在一起,不同纬度的风味会按照纬度定义的顺序进行组合。
多维度代码合并规则
那么不同纬度的代码是如何合并的呢?
写 free 风味的代码,我们可以像上面 huawei/xiaomi 风味一样,创建一个free风味的文件目录。
此时如何你在 free 定义一个和 huawei 或 xiaomi 风味目录下一样的类就会报错,类名有冲突。我们刚定义的时候 huawei 风味的纬度在 free 的纬度前面,我们可以认为定义在前面的纬度高,代码最终合并的时候,低纬度的代码向高纬度合并,也就是 free 的代码最终会合并到 huawei 或 xiaomi 风味上,此时低纬度定义了和高纬度相同的类名就会有冲突问题。
总结一下合并规则如下:
- 相同纬度的风味,类名可以相同,如上 huawei 、xiaomi 风味都属于 channel 纬度,他们属于平级关系,互不影响,可以有相同的类。
- 不同纬度不能有相同类名出现,否则会出现冲突问题。
- 对与资源文件及id ,不管是相同纬度还是不同纬度都可以相同。如果相同,那么低纬度会覆盖掉高纬度资源文件及id。
多纬度可以解决什么问题
还是上面联运的需求,现在我们只接入了华为和小米的联运,可能还有其它风味的、如 oppo 、vivo、meizu 等等。现在华为、小米风味的支付使用的是对应联运的支付,其他风味不太算接入联运或者暂时不接入,其他风味都用支付宝支付怎么办?
上面代码我们在 huawei 、xiaomi 风味目录下我们都定义了 PayHelper 这个类,不管我们切到 huawei 还是 xiaomi 风味都能找到这个类,那么如果切到 oppo 、vivo、meizu 则么办,main 中用到了这个类,但这些风味下我们没有定义这个类,编译肯定不通过。
🤔 那就在没个风味目录下定义这个类?
🤔 这些风味的 PayHelper 都是相同的代码都是支付宝支付方式,复制粘贴好像也不是很麻烦 ?
🤔 如果修改,那每个目录下都要修改,都是重复的代码,这不合理呀。
那就通过多纬度,改造一下吧。
flavorDimensions "channel","pay_type"
productFlavors {
// 渠道风味
huawei {dimension "channel"}
xiaomi {dimension "channel"}
oppo {dimension "channel"}
vivo {dimension "channel"}
//...
// 支付风味
huaweiPay{dimension "pay_type"}
xiaomiPay{dimension "pay_type"}
commonPay{dimension "pay_type"}
}
我们将 huawei支付的 PayHelper
写到 huaweiPay
风味目录下,小米支付的 PayHelper
写在 xiaomiPay
风味目录中,支付宝支付的 PayHelper
写在 commonPay
风味目录下,通过不同纬度风味组合就可以等到我们想要的 变种(variant )了。
但组合的时候,n*m 中排列组合,比如 vivo 和 huaweiPay 就会组合在一起,这是我们不想要的变体,我们可以过滤掉一些不需要的组合。
variantFilter { variant ->
//获取变体中的风味
def flavors = variant.flavors
def channelFlavor=flavors[0].name
def payTypeFlavor=flavors[1].name
switch (payTypeFlavor){
case "huaweiPay":
if (channelFlavor != "huawei") setIgnore(true)/*忽略此变体*/
break
case "xiaomiPay":
if (channelFlavor != "xiaomi") setIgnore(true)
break
case "commonPay":
if (channelFlavor == "huawei" || channelFlavor == "xiaomi") setIgnore(true)
break
}
}
Module 中的风味
依赖库中维度app中没有
一般大一点的项目,都会划分多个module,如果 module 有风味维度,app 中没有该如何处理呢?
build.gradle (app)
flavorDimensions "channel","pay_type"
productFlavors {
// 渠道风味
huawei {dimension "channel"}
//...
huaweiPay{dimension "pay_type"}
//...
}
build.gradle (mylibrary)
flavorDimensions "type"
productFlavors {
phone {dimension "type"}
pad {dimension "type"}
}
如果这个时候 app 去依赖 mylibrary 就会出现下面的错误。
我们需要在app 的风味中指定 mylibrary 使用该 type 维度的那种风味
我们可以在app 的 defaultConfig 进行统一配置
defaultConfig {
……
// 配置缺失维度策略,第一个参数是维度的名称,第二个维度是该维度对应的风味
missingDimensionStrategy "type", "phone"
}
也可以在具体风味中做特殊配置
flavorDimensions "channel","pay_type"
productFlavors {
// 渠道风味
huawei {
dimension "channel"
// 配置缺失维度策略,第一个参数是维度的名称,第二个维度是该维度对应的风味
missingDimensionStrategy "type", "pad"
}
//...
huaweiPay{dimension "pay_type"}
//...
}
我们可以利用上面的特性,去处理上面的支付需求,把支付需求写在 module 中,然后配置不同风味,在 app 中的不同风味中,通过 missingDimensionStrategy
指定 module 的不同风味进行组合。
依赖库维度和app维度相同,但风味不同
app 的 build.gradle 同上
build.gradle (mylibrary2)
flavorDimensions "pay_type"
productFlavors {
pay1 {dimension "pay_type"}
pay2 {dimension "pay_type"}
}
如果此时 app 去依赖 mylibrary2 ,会构建失败的,app 中没有和 mylibrary2 匹配的风味。此时可以通过 matchingFallbacks 进行配置,配置如下
productFlavors {
huawei {
dimension "channel"
missingDimensionStrategy "type", "pad"
}
xiaomi {
dimension "channel"
}
oppo {dimension "channel"}
vivo {dimension "channel"}
huaweiPay{
dimension "pay_type"
//这个必须在相同维度的配置
//如果mylibrary2 中有和此风味相同的类,是可以覆盖的,不会有冲突错误
matchingFallbacks = ['pay1']
}
...
}
以上是关于Android Gradle flavor —— 打造不同风味的app的主要内容,如果未能解决你的问题,请参考以下文章
Android Gradle flavor —— 打造不同风味的app
Android Gradle flavor —— 打造不同风味的app
如何在android studio中用gradle替换buildvariant的字符串?
如何使用Gradle中的不同资源文件夹为每个flavor创建两个应用程序?
Android开发:《Gradle Recipes for Android》阅读笔记(翻译)3.3——整合resource文件