Android多渠道productFlavors同时开发两个类似的app

前言

最近有个需求,老板让开发一个新的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:

图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:

图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代码和res资源是如何合并的?

java代码的合并比较简单,举个简单的例子,如图:

我们在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写在AndroidManifest下,多渠道下,就需要改造一番:

        
        

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就能使用不同的值了。

结语

多渠道开发算是小众开发方式,也正因为小众,网上也没有太多太详细的资料。我写这篇文章除了当做资料记忆之外,也希望能帮助到需要此功能的朋友们可以愉快的开发。

你可能感兴趣的:(Android多渠道productFlavors同时开发两个类似的app)