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 风味上,此时低纬度定义了和高纬度相同的类名就会有冲突问题。

总结一下合并规则如下:

  1. 相同纬度的风味,类名可以相同,如上 huawei 、xiaomi 风味都属于 channel 纬度,他们属于平级关系,互不影响,可以有相同的类。
  2. 不同纬度不能有相同类名出现,否则会出现冲突问题。
  3. 对与资源文件及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文件

Android 子 module 里使用 flavor 导致编译失败的问题