Android 差异化打包,一套代码打包多个不同的APP

3,196 阅读4分钟

当一个APP成熟起来,功能会越来越多,业务会越来越复杂,面向的用户群越来越大。这个时候为了更进一步的发展和扩大业务,我们可以对APP进行拆分,做成两个甚至更多个的APP,每个APP都服务特定的用户群。为了缩短开发时间,降低维护成本,肯定是不能再单独新建一个项目工程的。那怎么在原有的项目工程来进行开发,从而实现一套代码能够打出不同的APP呢?这就是本篇文章要介绍的“差异化打包”。

gradle配置如下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29

    defaultConfig {
        applicationId "com.github.productflavors"
        minSdkVersion 21
        targetSdkVersion 29
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    //差异化打包,名字任意起
    flavorDimensions 'normal'
    /*多渠道的一些配置 */
    productFlavors {
        normala {
            dimension = 'normal'
            // 设置applicationId(这里很重要,两个相同applicationId的apk不同同时安装在同一台Android手机中)
            applicationId = 'com.github.productflavorsnormala'
            targetSdkVersion 29
            maxSdkVersion 29
            minSdkVersion 21
//            signingConfig signingConfigs.release
            buildConfigField "String", "BASE_URL", "\"http://www.baidu.com\""
//            resValue "string", "app_name", "DemoA"//注释:须删除main下 res/values/Strings.xml 中 app_name
            manifestPlaceholders = [
                    app_name      : "DemoA",
                    app_icon      : "@mipmap/ic_launcher",
                    app_round_icon: "@mipmap/ic_launcher_round",
                    app_theme     : "@style/AAAA"//配置normalA的差异化主题
            ]

            versionCode 1
            versionName "1.0"

        }
        normalb {
            dimension = 'normal'
            applicationId = 'com.github.productflavorsnormalb'
            targetSdkVersion 29
            maxSdkVersion 29
            minSdkVersion 21
//            signingConfig signingConfigs.release
            buildConfigField "String", "BASE_URL", "\"http://www.qq.com\""
//            resValue "string", "app_name", "DemoB"//注释:须删除main下 res/values/Strings.xml 中 app_name
            manifestPlaceholders = [
                    app_name      : "DemoB",
                    app_icon      : "@mipmap/ic_launcher",
                    app_round_icon: "@mipmap/ic_launcher_round",
                    app_theme     : "@style/BBBB"//配置normalB的差异化主题
            ]

            versionCode 1
            versionName "1.0"

        }
    }
    
    //指定版本加载对应的代码or配置
    sourceSets {
        normala {
            manifest.srcFile '/src/normala/AndroidManifest.xml'//其中区分了主题和图标
        }
        normalb {
            manifest.srcFile '/src/normalb/AndroidManifest.xml'//其中区分了主题和图标
        }
    }

    buildTypes {

        debug {
//            signingConfig signingConfigs.config
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

        }

    }

    //签名文件配置: 这是第一种写法:
/*    signingConfigs {
        debug {
            storeFile file('../sign.jks')
            storePassword "123456"
            keyAlias "key0"
            keyPassword "123456"
            v1SigningEnabled true
            v2SigningEnabled true
        }
        release {
            storeFile file('../sign.jks')
            storePassword "123456"
            keyAlias "key0"
            keyPassword "123456"
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }*/

//  这是签名的第二种写法  调用getSigningProperties方法里面的函数,
//  通过读取里面的配置文件进行操作:
//

    signingConfigs {
        debug {
            v1SigningEnabled true
            v2SigningEnabled true
        }

        release {
            storeFile
            storePassword
            keyAlias
            keyPassword
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }

    getSigningProperties()

    //自定义输出包名的设置
    applicationVariants.all { variant ->
        //获取当前渠道
        def flavor = variant.productFlavors[0]
        //获取当前build版本
        def buildType = variant.buildType.name
        //获取当前时间的"YYYY-MM-dd"格式。
        def createTime = new Date().format("YYYY-MM-dd", TimeZone.getTimeZone("GMT+08:00"))
        variant.outputs.all {
            if (buildType == "release") {
                //输出apk名称为:渠道名_版本名_时间.apk
                def fileName = " ${flavor.name}-${variant.buildType.name}_v${flavor.versionName}_${createTime}.apk"
                outputFileName = fileName
            }
        }
    }

}

//读取签名配置文件
def getSigningProperties() {

    def propFile = file('sign.properties')
    if (propFile.canRead()) {
        def Properties props = new Properties()
        props.load(new FileInputStream(propFile))
        if (props != null && props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') &&
                props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) {

            android.signingConfigs.release.storeFile = file(props['STORE_FILE'])
            android.signingConfigs.release.storePassword = props['STORE_PASSWORD']
            android.signingConfigs.release.keyAlias = props['KEY_ALIAS']
            android.signingConfigs.release.keyPassword = props['KEY_PASSWORD']

        } else {

            println 'signing.properties found but some entries are missing'
            android.buildTypes.release.signingConfig = null
        }
    } else {
        println 'sign.properties not found'
        android.buildTypes.release.signingConfig = null
    }
}

//获取版本号:
def getVersionCode() {
    def versionFile = file('version.properties')
    if (versionFile.canRead()) {
        def Properties versionProps = new Properties()
        versionProps.load(new FileInputStream(versionFile))
        def versionCode = versionProps['VERSION_CODE'].toInteger()
        def runTasks = gradle.startParameter.taskNames
        //仅在assembleRelease任务是增加版本号
        if ('assembleRelease' in runTasks) {
            versionProps['VERSION_CODE'] = (++versionCode).toString()
            versionProps.store(versionFile.newWriter(), null)
        }
        return versionCode
    } else {
        throw new GradleException("Could not find version.properties!")
    }
}


//获取当前时间
def getCurrentTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

目录介绍

如上图所示,main 文件中的代码和资源是公共的,即normalA和normalB都有的功能。normalA 文件夹就是放normalA版APP特有的文件,而normalB 文件夹就是放normalB版APP特有的文件。

再看下每个版本中的代码:

公共部分:

package com.github.productflavors;

import android.content.Context;
import android.widget.Toast;

public class Toa {
    public static void toast(Context context,String msg){
        Toast.makeText(context,msg,Toast.LENGTH_LONG).show();
    }
}

差异化normalA部分:

package com.github.productflavors;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toa.toast(this, "A");
    }
}

差异化normalB部分

package com.github.productflavors;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toa.toast(this, "B");
    }
}

其实把MainActivity分在两个差异化的版本里是因为可能这两个类中所有逻辑都不一样, 所以把它抽出来放在差异化部分,假如MainActivity的主题配置每个版本都不一样,那么就可以在对应版本的 AndroidManifest.xml中进行配置

再看下AndroidManifest.xml 文件配置:

公共部分:

差异化normalA部分:

差异化normalB部分:

其中application节点中接收了gradle中多渠道差异化配置的 manifestPlaceholders 中定制的参数,其中normalA和normalB共同配置了不同的icon图标及MainActivity的差异化主题, 差异化的图标及主题要放在对应的差异化的部分中,这样在运行时就可以显示出gradle中配置的样式了。

可以看下差异化部分的主题配置:

normalA部分:

normalB部分:

可以看到主题的颜色并不一样。

在选择版本build时先按照下方图片中的方法切换到对应的版本:

最后看下效果:

差异化normalA部分:

差异化normalB部分:

点击HELLO WOLD!按钮跳转到共同的SecondActivity页面:

以上所演示的功能的相关代码已提交到 :ProductFlavors