Android多渠道productFlavors同时开发两个类似
前言
最近有个需求,老板让开发一个新的app,新的app上的功能和老的app基本上完全一致,差异化的地方很少,那按照惯性思维,复制出一个老的app,然后改改色值,icon,string不就可以了么。但是,还要求以后保持俩app数据同步,产品同步。换句话说,老app上有需求,新的app上也要同样去支持。这样的话,复制出的新app想要同步支持老app的需求,就比较难了,反之亦然。其实,google官方早就给出了类似问题的解决方案,多渠道打包。
build配置及目录结构
①新增productFlavors
在app的build中新增productFlavors
来标记多渠道:
flavorDimensions "app"
productFlavors {
main {
applicationId "com.zdu.client"
dimension "app"
}
app2 {
applicationId "com.zdu.test"
dimension "app"
}
}
flavorDimensions
官方文档上介绍的很清楚,这是一个维度标识。这么讲肯定难以理解,如上代码所示,flavorDimensions
只有一个值app
,这么编译完后查看 Build Variants
,如图1:
会有四个组合,分别是main的debug和release环境以及app2的debug和release环境。那如果我们再增加一个新的维度
lib
,会是什么样子的呢?上代码:
flavorDimensions "app","lib"
productFlavors {
main {
applicationId "com.zdu.client"
dimension "app"
}
app2 {
applicationId "com.zdu.test"
dimension "app"
}
app3 {
applicationId "com.zdu.test2"
dimension "lib"
}
}
sync后查看Build Variants
,如图2:
变成了mainApp3的debug和release环境以及app2App3的debug和release环境。
原因就是main和app2都是使用的app维度,所以他们俩是同维度的单位,而app3是lib维度属性,所以app3要分别和main,app2两个渠道进行组合,形成了一个新的维度单位。
PS:这个功能,大家理解了就行,目前我没找到可以使用的场景。一般来说,只需要保持一个维度就好。
②创建新渠道app2的文件目录
首先切换到Project目录访问,在app-src
的目录下,也就是说和main平级的目录下,创建app2
文件夹,然后在app2的目录下创建跟main目录下一模一样的文件目录结构,如图:
基于此,准备工作就算做好,基本配置和基本结构已经搞定,接下来就是来了解如何去多渠道开发。
使用技巧
再看下上图,app2和main目录结构是一样的,那是不是意味着,app2和main是平级的?切换到app2分支的时候就会走app2的java代码和res的资源呢?
先回答第一个问题:
app2和main是平级的?
app2和main并不是平级,相反的,app2是main的附属,main是公共代码资源库,app2的所有缺失的java和res资源都会去main下找公共资源,所以我们切换到app2渠道下,可以直接运行app,除了applicationId不同之外,app不会有任何变化。
main是公共代码资源库,这句话的意思是说,无论有多少个渠道,main下的java和res都是最基本的存在,类似于所有其他的渠道都在引用main这个库的意思。这和我们开发引用一个库是类似的原理,只是完全反转过来,我们开发一个库,是app来引用这个库,而多渠道下都在一个app下,其他渠道以类似引用的方式来使用main下的java和res。
切换到app2分支的时候就会走app2的java代码和res的资源呢?
如果理解了第一个问题,那第二个问题也就比较好理解了。app2作为main的附属,切换到app2分支后,会将app2下的java代码和res合并到main下编译运行。
随之又会有一个新的问题,java代码和res资源是如何合并的?
java代码的合并比较简单,举个简单的例子,如图:java代码和res资源是如何合并的?
我们在app2创建如图的目录结构,编译运行后,相当于在main下也创建了一样的目录结构,将app2下的代码复制一份到对应的目录结构下。如果在app2和main的相同目录结构都创建一样的类会怎样?如图
那么这就要求我们渠道下的java目录结构和类名不能和main公共资源下的完全一致。
res资源的合并相对来说就是真正的合并了,但drawable,layout,和values下的合并还有所不同。
drawable合并
drawable的合并只需要命名一致,并对比main项目中图片放置的位置放到tea项目的对应位置即可完成替换。
图片替换要注意两点:第一,目前和命名一致;第二:main下有几套图片,app2下就要有几套图片,可以多但不能少。
app2下新增一个main没有的图片,代码中去引用了的话,切换到main渠道下会报错找不到该资源文件,这个问题稍后讲解。
layout合并
laout布局文件跟drawable图片合并一样,也是要求命名一致,但涉及到布局文件中的id的处理,要求比较严格,如果相同的功能只是布局位置,字体大小,色值等调整,那么id必须一致,因为同一个java文件引用不同渠道下的layout布局,如果id不同,切换渠道肯定报错;如果app2中新增一个id,而又在java代码中引用了,那么切换到main渠道下也会报错,因为main渠道下的layout没有这个id,这块的处理稍后再说。
string,color合并
string和color等类似独一份的资源文件合并又有所不同,简单的说就是,相同命名的string和color会被替换,不同命名的会新增。如图:
image.png
image.png
相同的app_name就会被替换成MyApp2的名称。
不同命名的会新增,也会有layout布局id类似的问题,如果main下string.xml没有相同命名的资源,同时又在java代码中引用了,一样会出问题,这块稍后一起讲解。
java代码的差异化处理
java代码的差异化处理是重中之重,再怎么相似的俩app,总有些个别地方逻辑不同的地方。我这边提供两种处理差异化代码的方式:
main下公共代码库差异化处理
两个app共用一套代码的前提下,在main下进行代码区分,这种情况需要做渠道区分,BuildConfig
类中已经有渠道区分常量:BuildConfig.FLAVOR
那么在代码中就可以判断:
if ("main".equals(BuildConfig.FLAVOR)) {
// 处理main下逻辑
} else if ("app2".equals(BuildConfig.FLAVOR)) {
// 处理app2下逻辑
}
这里是建议大家写一个工具类,不然每个差异化的地方都要这么判断很蠢的。
public class FlavorUtils {
public static boolean isMain() {
return "main".equals(BuildConfig.FLAVOR);
}
public static boolean isApp2() {
return "app2".equals(BuildConfig.FLAVOR);
}
}
差异化不多的情况下,这种写法是最方便的,也是最效率的,唯一的坏处就是在于要多判断。
注:这种差异化处理是将main和app2分别当做一个独立的渠道,但因为main还是公共代码库,所以切换到app2下进行编译,会同时编译app2和main下的java代码,这种情况下main代码中引用app2的类是没有问题的。
但如果切换到main渠道下去编译,你会发现编译后提示找不到app2下类的错误,那是因为切换到main渠道下,只会编译main下java代码,不会编译app2的java代码,自然就找不到对应app2下的类了。解决方式也有:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
java.srcDirs = ['src/main/java','src/app2/java']
}
app2 {
java.srcDirs = ['src/app2/java']
}
}
配置main下的java.srcDirs
编译目录,切换到main渠道后同时编译main/java和app2/java
,就可以了。
分离公共代码库,每个app创建对应的渠道
在前文中,我们都是把main当做一个单独的app渠道,app2作为第二个渠道,现在的方式就是,将main的渠道单独分离出来,创建app1渠道。将app1和app2差异的类从main下剪切出来同时复制到对应的app1和app2下,单独去开发对应的渠道代码,互相不干扰。
这样,main的功能性就只是公共代码资源库的职能,不能再作为一个单独的渠道去编译运行了。但同时,build也需要修改下:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
java.srcDirs = ['src/main/java']
}
app1 {
java.srcDirs = ['src/app1/java']
}
app2 {
java.srcDirs = ['src/app2/java']
}
}
各自编译各自的java代码。
app1和app2下相同的类也不会报错:
原因很简单,因为编译了app1渠道,没有编译app2渠道,自然不会出现类冲突的问题。
注:这种java代码的差异化处理需要注意,main只能引用app1和app2下路径和类名一致的java类,互相切换渠道才不会报错,如果main只引用了app1中有的类,而app2下没有这个类,那切换到app2渠道下肯定要报错了。
gradle使用技巧
上面那些可以让我们顺利的写代码,但还不够。比如环境配置,签名配置,不同渠道下的各种三方key值,甚至不同环境都会有不同的key值等等,这些在正式开发中,肯定会遇到的。下面就给大家详细的介绍下,遇到这些问题,该怎么去处理。
三方key值配置
三方key值一般都是写在AndroidManifest
中的,如:
<!--微信id-->
<meta-data
android:name="WEIXIN_ID"
android:value="******************" />
单渠道下,我们可以直接把id写在AndroidManifest
下,多渠道下,就需要改造一番:
<!--微信id-->
<meta-data
android:name="WEIXIN_ID"
android:value="${WX_KEY}" />
gradle
中这样配置:
productFlavors {
main {
applicationId "com.zdu.client"
dimension "app"
manifestPlaceholders = [
WX_KEY : "*************",
]
}
app2 {
applicationId "com.zdu.test"
dimension "app"
manifestPlaceholders = [
WX_KEY : "%%%%%%%%%%%%%%%%%",
]
}
}
这样配置之后,就能分渠道加载不同的key值。
签名配置
多渠道下,仅支持debug多签名配置,不支持release的多签名配置,换句话说,release下只能配置一个签名。
首先,新增一个debug签名:
signingConfigs {
release {
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
storeFile STORE_FILE
storePassword STORE_PASSWORD
}
debug {
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
storeFile STORE_FILE
storePassword STORE_PASSWORD
}
app2Debug {
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
storeFile STORE_FILE
storePassword STORE_PASSWORD
}
}
然后配置签名引用:
buildTypes {
debug {
// main签名
productFlavors.main.signingConfig signingConfigs.debug
//app2签名
productFlavors.app2.signingConfig signingConfigs.app2Debug
}
}
由于只能配置debug环境的签名,不能配置release的签名,就导致不能多渠道多签名开发,只能共同使用一个签名。当然非要多签名开发也是可以的,就是每次换渠道手动改gradle文件,无非就是比较麻烦罢了。
不过话又说回来了,同一个公司的产品使用同一个签名文件是很常见的事件,能省去很多麻烦。
不同环境下的key值配置
这个技巧挺实用的,比如各种统计三方的key,往往都是测试环境和正式环境不同,这个时候就需要这种来配置了。
正常开发,一般最少会有俩环境,咱们先模拟一番:
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
signingConfig signingConfigs.release
}
}
在这里面如果想跟设置签名的那种直接配置各种参数是不行的,这里就不举例子了,不信的话各位可以试试。
这里要使用另外一种方式:
android.applicationVariants.all { variant ->
println(variant.name)
if (variant.name == 'mainDebug') {
buildConfigField "Integer", "ENV", "2"
}
if (variant.name == 'mainRelease') {
buildConfigField "Integer", "ENV", "0"
}
if (variant.name == 'app2Debug') {
buildConfigField "Integer", "ENV", "2"
}
if (variant.name == 'app2Release') {
buildConfigField "Integer", "ENV", "0"
}
}
variant.name
就是图里面对应的名称,环境不同,只需要在这个判断里写对应环境的值,然后在BuildConfig.ENV
就能使用不同的值了。