Android应用构建-BuildType ProductFlavor

个人原创,欢迎指正

场景:

用同一套核心代码来维护多个订制的app
现开发出一套Android app, 在此app基础上,复制出另一个app, 两个app之间略微有一些不同之处。
将不同环境的APP安装在同一设备中。

使用Gradle构建的优点:

提高效率,产品未成熟,包括需求上的修改及各种BUG,用一套代码来维护,可以减少不必要的重复劳动
免除传统的重复修改环境,打包,安装繁琐的流程,期间漏改在所难免。而使用Gradle构建,只需选择环境-安装。快捷,方便,不出错。

实现需求:

  • 两个APP, 用一套代码来维护,不同之处在于应用名称,图标,包名,部分跳转逻辑,部分页面,部分页面显示文字。

  • 三个环境: 开发, 测试, 生产, 每个环境的版本,版本号,图标不同

  • 命名规则:ProductFlavor_产品版本号_日期_BuildType.apk
    whgc_1.0.0_20190624_Dev.apk
    whgc_1.0.0_20190624_Test.apk
    whgc_1.0.0_20190624_Release.apk

  • 各环境APP可安装在同一手机

环境

AndroidStudio 3.4
Gradle插件:3.4.0
Gradle:5.1.1

三个环境 实现步骤:

  • 新建项目
    app: build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.minicup.test"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    ...
}
  • app: build目录结构


    image.png
  • 执行assemble任务

  • 任务执行后在build目录中生成相应文件


    image.png

assemble任务会自动创建debug和release两个目录
这里的debug和release目录对应build.gradle中buildTypes的配置

可项目中buildTypes中明明只有release,并没有debug,原因是这里的release和debug是系统默认的两个构建类型,即使没有配置,系统依然会生成这两个目录
修改一下build.gradle,将release配置也删除,再次运行assemble任务,发现同样生成了release和debug两个目录

android {
    buildTypes {
    }
}

接下来在buildTypes中添加个dev 类型

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        dev{
        }
    }
}

同步后首先看到变化如下:


image.png

image.png

再次执行assemble任务:


image.png

以上符合预期。

自此在buildTypes中,我们可以配置三种环境,解决关于三个环境的需求

android {
     ...
    buildTypes {
        appDev{
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        appTest{
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        appRelease {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

也可以这样配置

android {
    ...
    buildTypes {
//            常用配置:
//            signingConfig,
//            buildConfigField,
//            versionNameSuffix
//            multiDexEnabled,
//            zipAlignEnabled,
//            applicationIdSuffix
        debug{
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }

        appDev{
            initWith(debug)
        }

        appTest{
            initWith(debug)
        }

        appRelease {
            initWith(debug)
        }
    }
}

assemble后的结果


image.png

我们也可以打开ProjectStructure,在这里配置所有可以配置的东西


image.png

assemble是构建所有buildTypes productFlavors嵌套配置生成的目录。
buildTypes中的配置命名不能以test开头, //BuildType names cannot start with 'test'
除了debug,其他构建类型都是release类型的,因此,在没有设置签名配置时,会生成含有unsigned的标志。
所有的module必须有同样的构建类型,即使是空的

为三个环境指定签名文件,及不同的BASE_URL

先生成jks文件,后配置build.gradle

android {
    signingConfigs {
        release {
            storeFile file('app.jks')
            storePassword '123456'
            keyAlias = 'app'
            keyPassword '123456'
        }
    }
   
    ...

    buildTypes {
        debug{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/debug\""
        }

        appDev{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appDev\""
            signingConfig signingConfigs.release
        }

        appTest{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appDev\""
            signingConfig signingConfigs.release
        }

        appRelease {
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appRelease\""
            signingConfig signingConfigs.release
        }
    }
}

编译后,生成相应的BuildConfig,里面的内容与我们配置的一致


image.png

运行APP


image.png

EventLog

11:07   Executing tasks: [:app:assembleDebug]
11:07   Gradle build finished in 4 s 175 ms
11:07   Install successful

默认运行app时,执行的是assembleDebug任务。
通过设置BuildVariants来切换任务。


image.png

切换为appDev构建类型后运行app的效果:


image.png

EventLog

11:19   Executing tasks: [:app:assembleAppDev]
11:19   Gradle build finished in 6 s 488 ms
11:19   Install successful

对应的构建类型BuildConfig中生成了不同的值,运行 appTest, appRelease也得到相应的结果。

但在运行APP时出现一个问题,也是上面我们需要解决的需求之一, 三种BuildTypes运行时,生成的APP会被替换掉,最后运行三种BuildTypes在手机中只生成一个APP。如何让开发,测试,生产环境的APP存在于同一个手机上。为了解决这个问题,我们先来看看如何为不同的环境指定不同的应用名称及图标,用以安装后展示效果

为三个环境指定不同的应用名称,应用图标

按照开发-测试-生产的顺序准备三张应用图标


ic_launcher_dev.png
ic_launcher_test.png
ic_launcher_release.png

strings.xml

<resources>
    <string name="app_name">test</string>
    <string name="app_name_dev">DEV</string>
    <string name="app_name_test">TEST</string>
    <string name="app_name_release">RELEASE</string>
</resources>
android {
    ...
    buildTypes {

        debug{
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/debug\""
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher",
                    app_label : "@string/app_name"
            ]
        }

        release{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/release\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher",
                    app_label : "@string/app_name"
            ]
        }

        appDev{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appDev\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher_dev",
                    app_label : "@string/app_name_dev"
            ]
        }

        appTest{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appTest\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher_test",
                    app_label : "@string/app_name_test"
            ]
        }

        appRelease {
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appRelease\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher_release",
                    app_label : "@string/app_name_release"
            ]
        }
    }
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.minicup.test">

    <application
        android:allowBackup="true"
        android:icon="${app_icon}"
        android:label="${app_label}"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

通过设置不同的BuildVariants获得的结果:

BuildType.appDev
BuildType.appTest
BuildType.appRelease

以上结果针对BuildType的应用名称,应用图标,及打开APP中的BASE_URL都一一对应。接下来让三种BuildType运行在一台机器上。

三种不同的BuildType,安装在同一设备上。

android {
    ...
    buildTypes {
         ...
        appDev{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appDev\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher_dev",
                    app_label : "@string/app_name_dev"
            ]
            applicationIdSuffix = ".dev"
        }

        appTest{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appTest\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher_test",
                    app_label : "@string/app_name_test"
            ]
            applicationIdSuffix = ".test"
        }

        appRelease {
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appRelease\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@mipmap/ic_launcher_release",
                    app_label : "@string/app_name_release"
            ]
            applicationIdSuffix = ".release"
        }
    }
}

切换三种BuildType,安装APP到设备中:


image.png

实现一套代码维护两个应用,每个应用都有dev,test,release三个构建类型,两个应用的图标,名称,包名不同

  • app build.gradle 指定不同productFlavor下的包名,及不同buildType下的图标,名称及包名后缀
apply plugin: 'com.android.application'

android {
    signingConfigs {
        release {
            storeFile file('appDev.jks')
            storePassword '123456'
            keyAlias = 'appDev'
            keyPassword '123456'
        }
    }
    compileSdkVersion 28

    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        flavorDimensions "default" 
    }
    buildTypes {
        appDev{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appDev\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@drawable/ic_launcher_dev",
                    app_label : "@string/app_name_dev"
            ]
            applicationIdSuffix = ".dev"
        }

        appTest{
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appTest\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@drawable/ic_launcher_test",
                    app_label : "@string/app_name_test"
            ]
            applicationIdSuffix = ".test"
        }

        appRelease {
            buildConfigField "String", "BASE_URL", "\"http://192.168.0.1:8080/appRelease\""
            signingConfig signingConfigs.release
            manifestPlaceholders  = [
                    app_icon :"@drawable/ic_launcher_release",
                    app_label : "@string/app_name_release"
            ]
            applicationIdSuffix = ".release"
        }
    }

    productFlavors {
        orange {
            applicationId "com.minicup.orange"
        }

        green {
            applicationId "com.minicup.green"
        }

    }

    //去掉默认的debug及release两个buildType
    variantFilter { variant ->
        if(variant.buildType.name.equals('release') || variant.buildType.name.equals('debug')) {
            variant.setIgnore(true)
        }
    }
}

...

*创建以下的目录结构,不同productFlavor对应相应的图标,名称


image.png

green目录下的图标及名称:


ic_launcher_dev.xml
ic_launcher_test.xml
ic_launcher_release.xml
<resources>
    <string name="app_name_dev">DEV_G</string>
    <string name="app_name_test">TEST_G</string>
    <string name="app_name_release">RELEASE_G</string>
</resources>

orange目录下的图标及名称:


ic_launcher_dev.xml
ic_launcher_test.xml
ic_launcher_release.xml
<resources>
    <string name="app_name_dev">DEV_O</string>
    <string name="app_name_test">TEST_O</string>
    <string name="app_name_release">RELEASE_O</string>
</resources>
  • MainActivity
public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.textView);
        mTextView.setText(BuildConfig.FLAVOR + " "+ BuildConfig.BUILD_TYPE);
    }
}
  • 同步项目,查看BuildVariant, 生成了两个应用对应的构建类型 2(productFlavor)*3(buildType)=6(buildVariant)


    image.png
  • 执行assemble任务:


    image.png
  • 验证APP,将生成的6个APP安装到设备中


    image.png
  • 不同的productFlavor,不同的buildType生成的应用可安装到同一个设备
  • 同一个productFlavor的不同buildType,根据之前配置生成不同名称,不同图标,不同包名的app
image.png
image.png
image.png
image.png
image.png
image.png

设置生成APP的文件名称

    productFlavors {
        orange {
            applicationId "com.minicup.orange"
            versionCode 2
            versionName "2.2"
        }

        green {
            applicationId "com.minicup.green"
            versionCode 3
            versionName "3.3"
        }
    }


    applicationVariants.all { variant ->
        variant.outputs.all { output ->
                //产品名称_产品版本号_日期_软件环境
                outputFileName  = "${variant.productFlavors[0].name}_" +
                        "v${variant.versionName}_" +
                        "${releaseTime()}_" +
                        "${variant.buildType.name}" +
                        ".apk"
        }
    }


def releaseTime(){
    return new Date().format("yyyy-MM-dd")
}

执行assemble任务:

image.png

集成JPush时,遇到的问题

按照以上方法配置JPush时,打好的包,不能同时安装在同一个设备中


image.png

目前只能准备两台设备,同一设备只能安装同一buildtype的应用

参考:
https://developer.android.google.cn/studio/build/index.html
http://google.github.io/android-gradle-dsl/current/index.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容