在文章 使用 Gradle 对应用进行个性化定制 中,我们能够针对一个应用的正式服、测试服、超管服等其他版本,进行个性化定制。
这一篇文章我们来点大动作,让你用一套代码构建多个应用。

场景介绍

需求:“将某个应用换一套皮肤、第三方账号、后台服务器,改个名字上线,并且以后的新功能同步进行更新”。

当你遇到这样的需求会怎么做呢?

是将项目复制一份,然后修改其中的内容,有新功能的时候再手动复制过来稍微修改一下 UI?

或者可以切换一个分支,在这个分支上修改相关的信息,每次开发完新功能,将代码合并过来,再稍微修改新功能的 UI?

现在我来介绍使用 GradleflavorDimensions,实现一份代码构建多个应用。

具体实现

老规矩,先上完整的 Gradle 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
minSdkVersion 16
targetSdkVersion 25
versionCode gitVersionCode()
}

// 配置两个应用的签名文件
signingConfigs {
app1 {
storeFile file("app1.jks")
storePassword "111111"
keyAlias "app1"
keyPassword "111111"
}

app2 {
storeFile file("app2.jks")
storePassword "111111"
keyAlias "app2"
keyPassword "111111"
}
}

buildTypes {
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
}

debug {
// 显示Log
buildConfigField "boolean", "LOG_DEBUG", "true"
versionNameSuffix "-debug"
signingConfig null
manifestPlaceholders.UMENG_CHANNEL_VALUE = "test"
}
}

//创建两个维度的 flavor
flavorDimensions "APP", "SERVER"

productFlavors {

app1 {
dimension "APP"
applicationId 'com.imliujun.app1'

versionName rootProject.ext.APP1_versionName

//应用名
resValue "string", "app_name", "APP1"

buildConfigField("String", "versionNumber", "\"${rootProject.ext.APP1_versionName}\"")

//第三方SDK的一些配置
buildConfigField "int", "IM_APPID", "app1的腾讯IM APPID"
buildConfigField "String", "IM_ACCOUNTTYPE", "\"app1的腾讯IM accountype\""
manifestPlaceholders = [UMENG_APP_KEY : "app1的友盟 APP KEY",
UMENG_CHANNEL_VALUE: "app1默认的渠道名",
XG_ACCESS_ID : "app1信鸽推送ACCESS_ID",
XG_ACCESS_KEY : "app1信鸽推送ACCESS_KEY",
QQ_APP_ID : "app1的QQ_APP_ID",
AMAP_KEY : "app1的高德地图key",
APPLICATIONID : applicationId]
//签名文件
signingConfig signingConfigs.app1
}

app2 {
dimension "APP"
applicationId 'com.imliujun.app2'

versionName rootProject.ext.APP2_versionName

//应用名
resValue "string", "app_name", "APP2"

buildConfigField "String", "versionNumber", "\"${rootProject.ext.APP2_versionName}\""

//第三方SDK的一些配置
buildConfigField "int", "IM_APPID", "app2的腾讯IM APPID"
buildConfigField "String", "IM_ACCOUNTTYPE", "\"app2的腾讯IM accountype\""
manifestPlaceholders = [UMENG_APP_KEY : "app2的友盟 APP KEY",
UMENG_CHANNEL_VALUE: "app2默认的渠道名",
XG_ACCESS_ID : "app2信鸽推送ACCESS_ID",
XG_ACCESS_KEY : "app2信鸽推送ACCESS_KEY",
QQ_APP_ID : "app2的QQ_APP_ID",
AMAP_KEY : "app2的高德地图key",
APPLICATIONID : applicationId]
//签名文件
signingConfig signingConfigs.app2
}

offline {
dimension "SERVER"

versionName getTestVersionName()
}

online {
dimension "SERVER"
}

admin {
dimension "SERVER"

versionName rootProject.ext.versionName + "-管理员"
manifestPlaceholders.UMENG_CHANNEL_VALUE = "admin"
}
}
}

android.applicationVariants.all { variant ->
switch (variant.flavorName) {
case "app1Admin":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://admin.app1domain.com/\""
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getTestVersionName() + "-管理员")
} else {
variant.mergedFlavor.setVersionName(rootProject.ext.APP1_VERSION_NAME + "-管理员")
}
break
case "app1Offline":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://offline.app1domain.com/\""
variant.mergedFlavor.setVersionName(getTestVersionName())
break
case "app1Online":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://online.app1domain.com/\""
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getTestVersionName())
}
break
case "app2Admin":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://admin.app2domain.com/\""
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getApp2TestVersionName() + "-管理员")
} else {
variant.mergedFlavor.setVersionName(rootProject.ext.APP2_VERSION_NAME + "-管理员")
}
break
case "app2Offline":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://offline.app2domain.com/\""
variant.mergedFlavor.setVersionName(getApp2TestVersionName())
break
case "app2Online":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://online.app2domain.com/\""
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getApp2TestVersionName())
}
break
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ext {
APP1_VERSION_NAME = "2.0.2"
APP1_TEST_NUM = "0001"
APP2_VERSION_NAME = "1.0.5"
APP2_TEST_NUM = "0005"
}

def getTestVersionName() {
return String.format("%s.%s", rootProject.ext.APP1_VERSION_NAME,
rootProject.ext.APP1_TEST_NUM)
}

def getApp2TestVersionName() {
return String.format("%s.%s", rootProject.ext.APP2_VERSION_NAME,
rootProject.ext.APP2_TEST_NUM)
}

static int gitVersionCode() {
def count = "git rev-list HEAD --count".execute().text.trim()
return count.isInteger() ? count.toInteger() : 0
}

在上一篇文章的配置上进行了一些修改,同时保留上一篇文章里所有的功能。

配置多应用

首先来看最重要的一个概念:

1
flavorDimensions "APP", "SERVER"

这一行代码配置了两个维度的 flavorAPP 代表多应用,SERVER 代表服务器版本。

根据上面的配置信息可以看到,app1app2 设置了 dimension "APP" 所以属于 APP 这个维度,offlineonlineadmin 设置了 dimension "SERVER" 属于 SERVER 这个维度。

根据 Product Flavors 的两个维度 APP [app1, app2] 和 SERVER [offline, online, admin] 以及 Build Type [debug, release],最后会生成以下 Build Variant:

  • app1AdminDebug
  • app1AdminRelease
  • app1OfflineDebug
  • app1OfflineRelease
  • app1OnlineDebug
  • app1OnlineRelease
  • app2AdminDebug
  • app2AdminRelease
  • app2OfflineDebug
  • app2OfflineRelease
  • app2OnlineDebug
  • app2OnlineRelease

是不是每个应用都有 3 个服务器版本,每个版本都有 debugrelease 包。

配置不同的包名

我们要实现多应用,必须能安装在同一台手机上。所以不同应用之间的包名得不一样。

APP 维度的 flavor 中设置不同的 applicationId,就可以实现修改应用包名。

1
2
3
4
5
6
7
app1{
applicationId 'com.imliujun.app1'
}

app2{
applicationId 'com.imliujun.app2'
}

这样配置后,app1app2 就能够安装在同一台手机上,也能同时上传应用商店。

有一点大家切记,AndroidManifest.xml 中的 package 不需要去修改,R 文件的路径是根据这个 package 来生成的。如果对 package 进行修改,R 文件的路径也会改变,所有引用到 R 文件的类都需要进行修改。

动态配置 URL 和版本号

既然每个 Build Variant 都是由不同维度的 Product Flavors 和 Build Type 组合而来,我们肯定不能像上一篇文章一样将服务器的 URL 配置在 offlineonlineadmin 中了,因为 app1Offlineapp2Offline 同样是测试服,但不是同一个应用 URL 也不一样。

这个时候就需要通过 task 操作来根据不同的组合设置不同的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
android.applicationVariants.all { variant ->
//判断当前的 flavorName 是什么版本
switch (variant.flavorName) {
case "app1Admin":
//这是 app1 的超管版本,设置超管服务器 URL
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://admin.app1domain.com/\""
//判断当前是 `debug` 包还是 `release` 包,设置版本号
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getTestVersionName() + "-管理员")
} else {
variant.mergedFlavor.setVersionName(rootProject.ext.APP1_VERSION_NAME + "-管理员")
}
break
case "app1Offline":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://offline.app1domain.com/\""
variant.mergedFlavor.setVersionName(getTestVersionName())
break
case "app1Online":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://online.app1domain.com/\""
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getTestVersionName())
}
break
case "app2Admin":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://admin.app2domain.com/\""
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getApp2TestVersionName() + "-管理员")
} else {
variant.mergedFlavor.setVersionName(rootProject.ext.APP2_VERSION_NAME + "-管理员")
}
break
case "app2Offline":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://offline.app2domain.com/\""
variant.mergedFlavor.setVersionName(getApp2TestVersionName())
break
case "app2Online":
variant.buildConfigField "String", "DOMAIN_NAME",
"\"https://online.app2domain.com/\""
if ("debug" == variant.buildType.getName()) {
variant.mergedFlavor.setVersionName(getApp2TestVersionName())
}
break
}
}

两个 APP 的服务器 URL 和版本号不一致,所以通过 task 来动态设置。

配置应用名

不同的应用配置自己的应用名:

1
resValue "string", "app_name", "APP1"

这行代码的意思和在 strings.xml 中定义一个 String 值是一样的。不过这里通过 Gradle 配置了 app_name 就不能在 strings.xml 中再定义了,会报错提示有冲突。

配置应用签名

如果多个应用使用同一个签名文件,按照上一篇文章写的在 buildTypesreleasedebug 中配置就可以。但是每个应用的签名文件不一样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signingConfigs {

app1 {
storeFile file("app1.jks")
storePassword "111111"
keyAlias "app1"
keyPassword "111111"
}

app2 {
storeFile file("app2.jks")
storePassword "111111"
keyAlias "app2"
keyPassword "111111"
}
}

配置多个签名文件,在 APP 这个维度的 flavor 中配置签名信息:

1
2
3
4
5
6
7
app1{
signingConfig signingConfigs.app1
}

app2{
signingConfig signingConfigs.app2
}

这样就可以针对不同的应用设置不同的签名文件了。但是,还有一个要注意的地方,这个坑我以前没填上,而是绕远路绕过去了,现在我来填上它!

1
2
3
debug {
signingConfig null
}

一定要在 debug 中将签名文件的配置置空,不然 Build Type 的权限比 Product Flavors 要高,而 debug Build Type(构建类型) 会自动使用 debug SigningConfig (签名配置),这样一来就将 flavor 中配置的签名信息给覆盖掉了。导致的问题就是编译 release 包没有问题,编译 debug 包就不能使用某些需要校验签名的第三方SDK了。

配置不同应用的代码和资源

终于来到重头戏了,现在只需要更换 UI、文案或者某些界面布局和逻辑代码就大功告成啦。

首先,建立每个应用对应的 sourceSets 目录,比如:

  • app1 的 sourceSets 位置是 src/app1/
  • app2 的 sourceSets 位置是 src/app2/

app1 是已经开发完成的应用,只需要换 UI、文案就成了 app2,在 src/app2/ 目录下再新建 res 目录,将需要替换的切图命名和 app1 中的命名保持一致放入 res 对应的目录下就完美换肤了。

文案同理,将需要替换的字符串在 src/app2/res/values/strings.xml 中再写一份,保持 name 相同,其中的内容随便替换。

布局文件、style、color 替换的规则同上。

微信登录、分享、支付的回调是返回到 {应用包名.wxapi.WXEntryActivity}{应用包名.wxapi.WXPayEntryActivity} 这两个 Activity。

我们在 app1app2 中都放入这两个回调 Activity:

sourceSets 文件目录

然后在 AndroidManifest.xml 文件中动态配置 Activity 的包名:

1
2
3
4
<!-- 微信分享回调 -->
<activity android:name="${APPLICATIONID}.wxapi.WXEntryActivity"/>
<!-- 微信支付的回调 -->
<activity android:name="${APPLICATIONID}.wxapi.WXPayEntryActivity"/>

APPLICATIONID 占位符在 Gradle 中设置:

1
manifestPlaceholders = [APPLICATIONID : applicationId]

如果使用了 ShareSDK 做第三方分享和登录,需要配置 ShareSDK.xml 放到 assets 文件夹下,将 main/assets/ShareSDK.xml 复制一份到 app2/assets/ShareSDK.xml,将里面的第三方 APP ID 和 APP KEY 替换一下就可以了。

项目如果使用了 ContentProvider 要注意替换 authorities,如果 authorities 里面的值是一样的,手机上只能装一个应用哦,可以和上面动态配置 Activity 包名一样操作,用信鸽 SDK 演示一下:

1
2
3
4
5
 <!-- 【必须】 【注意】authorities修改为 包名.AUTH_XGPUSH, 如demo的包名为:com.qq.xgdemo -->
<provider
android:name="com.tencent.android.tpush.XGPushProvider"
android:authorities="${APPLICATIONID}.AUTH_XGPUSH"
android:exported="true"/>

总结

上面的内容基本涉及到所有的方面,其他的细节也好,特殊的需求定制也好,使用上面的方式去处理都能够解决。希望大家不要光学会复制粘贴,要掌握其原理,遇到类似的需求就能举一反三。

demo地址:https://github.com/imliujun/GradleTest

总结一下技术点:

  • manifestPlaceholders -> AndroidManifest.xml 占位符
  • buildConfigField -> BuildConfig 动态配置常量值
  • resValue -> String.xml 动态配置字符串
  • signingConfigs -> 配置签名文件
  • productFlavors -> 产品定制多版本
  • flavorDimensions -> 为产品定制设置多个维度
  • android.applicationVariants -> 操作 task

相关阅读

欢迎关注微信公众号:大脑好饿,更多干货等你来尝

公众号:大脑好饿