使用 Gradle 实现一套代码开发多个应用
在文章 使用 Gradle 对应用进行个性化定制 中,我们能够针对一个应用的正式服、测试服、超管服等其他版本,进行个性化定制。
这一篇文章我们来点大动作,让你用一套代码构建多个应用。
场景介绍
需求:“将某个应用换一套皮肤、第三方账号、后台服务器,改个名字上线,并且以后的新功能同步进行更新”。
当你遇到这样的需求会怎么做呢?
是将项目复制一份,然后修改其中的内容,有新功能的时候再手动复制过来稍微修改一下 UI?
或者可以切换一个分支,在这个分支上修改相关的信息,每次开发完新功能,将代码合并过来,再稍微修改新功能的 UI?
现在我来介绍使用 Gradle 的 flavorDimensions,实现一份代码构建多个应用。
具体实现
老规矩,先上完整的 Gradle 配置:
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
}
}
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
}
在上一篇文章的配置上进行了一些修改,同时保留上一篇文章里所有的功能。
配置多应用
首先来看最重要的一个概念:
flavorDimensions "APP", "SERVER"
这一行代码配置了两个维度的 flavor,APP 代表多应用,SERVER 代表服务器版本。
根据上面的配置信息可以看到,app1、app2 设置了 dimension "APP" 所以属于 APP 这个维度,offline、online、admin 设置了 dimension "SERVER" 属于 SERVER 这个维度。
根据 Product Flavors 的两个维度 APP [app1, app2] 和 SERVER [offline, online, admin] 以及 Build Type [debug, release],最后会生成以下 Build Variant:
app1AdminDebugapp1AdminReleaseapp1OfflineDebugapp1OfflineReleaseapp1OnlineDebugapp1OnlineReleaseapp2AdminDebugapp2AdminReleaseapp2OfflineDebugapp2OfflineReleaseapp2OnlineDebugapp2OnlineRelease
是不是每个应用都有 3 个服务器版本,每个版本都有 debug 和 release 包。
配置不同的包名
我们要实现多应用,必须能安装在同一台手机上。所以不同应用之间的包名得不一样。
在 APP 维度的 flavor 中设置不同的 applicationId,就可以实现修改应用包名。
app1{
applicationId 'com.imliujun.app1'
}
app2{
applicationId 'com.imliujun.app2'
}
这样配置后,app1 和 app2 就能够安装在同一台手机上,也能同时上传应用商店。
有一点大家切记,AndroidManifest.xml 中的 package 不需要去修改,R 文件的路径是根据这个 package 来生成的。如果对 package 进行修改,R 文件的路径也会改变,所有引用到 R 文件的类都需要进行修改。
动态配置 URL 和版本号
既然每个 Build Variant 都是由不同维度的 Product Flavors 和 Build Type 组合而来,我们肯定不能像上一篇文章一样将服务器的 URL 配置在 offline、online、admin 中了,因为 app1Offline 和 app2Offline 同样是测试服,但不是同一个应用 URL 也不一样。
这个时候就需要通过 task 操作来根据不同的组合设置不同的数据了。
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 来动态设置。
配置应用名
不同的应用配置自己的应用名:
resValue "string", "app_name", "APP1"
这行代码的意思和在 strings.xml 中定义一个 String 值是一样的。不过这里通过 Gradle 配置了 app_name 就不能在 strings.xml 中再定义了,会报错提示有冲突。
配置应用签名
如果多个应用使用同一个签名文件,按照上一篇文章写的在 buildTypes 的 release 和 debug 中配置就可以。但是每个应用的签名文件不一样呢?
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 中配置签名信息:
app1{
signingConfig signingConfigs.app1
}
app2{
signingConfig signingConfigs.app2
}
这样就可以针对不同的应用设置不同的签名文件了。但是,还有一个要注意的地方,这个坑我以前没填上,而是绕远路绕过去了,现在我来填上它!
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。
我们在 app1 和 app2 中都放入这两个回调 Activity:
然后在 AndroidManifest.xml 文件中动态配置 Activity 的包名:
<!-- 微信分享回调 -->
<activity android:name="${APPLICATIONID}.wxapi.WXEntryActivity"/>
<!-- 微信支付的回调 -->
<activity android:name="${APPLICATIONID}.wxapi.WXPayEntryActivity"/>
APPLICATIONID 占位符在 Gradle 中设置:
manifestPlaceholders = [APPLICATIONID : applicationId]
如果使用了 ShareSDK 做第三方分享和登录,需要配置 ShareSDK.xml 放到 assets 文件夹下,将 main/assets/ShareSDK.xml 复制一份到 app2/assets/ShareSDK.xml,将里面的第三方 APP ID 和 APP KEY 替换一下就可以了。
项目如果使用了 ContentProvider 要注意替换 authorities,如果 authorities 里面的值是一样的,手机上只能装一个应用哦,可以和上面动态配置 Activity 包名一样操作,用信鸽 SDK 演示一下:
<!-- 【必须】 【注意】authorities修改为 包名.AUTH_XGPUSH, 如demo的包名为:com.qq.xgdemo -->
<provider
android:name="com.tencent.android.tpush.XGPushProvider"
android:authorities="${APPLICATIONID}.AUTH_XGPUSH"
android:exported="true"/>
总结
上面的内容基本涉及到所有的方面,其他的细节也好,特殊的需求定制也好,使用上面的方式去处理都能够解决。希望大家不要光学会复制粘贴,要掌握其原理,遇到类似的需求就能举一反三。
总结一下技术点:
manifestPlaceholders->AndroidManifest.xml占位符buildConfigField->BuildConfig动态配置常量值resValue->String.xml动态配置字符串signingConfigs-> 配置签名文件productFlavors-> 产品定制多版本flavorDimensions-> 为产品定制设置多个维度android.applicationVariants-> 操作 task
补充
项目代码结构图
项目第一层目录结构

具体代码目录结构

相关阅读
欢迎关注微信公众号:大脑好饿,更多干货等你来尝
使用 Gradle 实现一套代码开发多个应用
注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权
使用 Gradle 实现一套代码开发多个应用的更多相关文章
- 一套代码小程序&Web&Native运行的探索07——mpvue简单调研
前言 接上文:[一套代码小程序&Native&Web阶段总结篇]可以这样阅读Vue源码 最近工作比较忙,加之上个月生了小孩,小情人是各种折腾他爸妈,我们可以使用的独立时间片不多,虽然这 ...
- 【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码
前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...
- 一套代码小程序&Web&Native运行的探索06——组件系统
接上文:一套代码小程序&Web&Native运行的探索05——snabbdom 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tre ...
- uni-app — 一套前端开发跨平台应用的终极解决方案
uni-app 是一个使用 Vue.js 开发跨平台应用的前端框架,开发者编写一套代码,可编译到iOS.Android.H5.小程序等多个平台. 今天有空就来介绍一下uni-app这个能够跨平台开发, ...
- 写出gradle风格的groovy代码
写出gradle风格的groovy代码 我们先来看一段gradle中的代码: buildscript { repositories { jcenter() } dependencies { class ...
- 从零开始搭建Electron+Vue+Webpack项目框架,一套代码,同时构建客户端、web端(一)
摘要:随着前端技术的飞速发展,越来越多的技术领域开始被前端工程师踏足.从NodeJs问世至今,各种前端工具脚手架.服务端框架层出不穷,“全栈工程师”对于前端开发者来说,再也不只是说说而已.在NodeJ ...
- APICloud发布低代码开发平台
云原生的出现,致使传统IT模式正在集中向云架构.云开发转型,其中在企业业务的互联网化.数字化进程中尤为突出,并衍生出“敏捷开发”.“快速迭代”的刚性需求.面对双模IT,如何打造全新的IT团队与模式?并 ...
- Android | 教你如何用代码开发一个拍照翻译小程序
引子 想必有很多小伙伴喜欢外出旅游,能去海外玩一圈那是更好不过了,旅游前大家一定会对吃.穿.住.行.游玩路线做各种攻略,然后满怀期待的出发- 想象中的旅游 出发前,想象中的旅游目的地可能有漂亮 ...
- 开源电影项目源码案例重磅分析,一套代码发布小程序、APP平台多个平台
uni-app-Video GitHub地址:https://github.com/Tzlibai/uni-app-video 一个优秀的uni-app案例,旨在帮助大家更快的上手uni-app,共同 ...
随机推荐
- JZYZOJ1383 [usaco2003feb]impster 位运算 最短路
http://172.20.6.3/Problem_Show.asp?id=1383 找能到达某个状态的最小操作数,然后把所有状态扫一遍即可,要额外判定一下起始就有的状态(如果起始里没有0那么这些状 ...
- Gauss 消元(模板)
/* title:Gauss消元整数解/小数解整数矩阵模板 author:lhk time: 2016.9.11 没学vim的菜鸡自己手打了 */ #include<cstdio> #in ...
- 浙南联合训练赛 D - Broken Clock
You are given a broken clock. You know, that it is supposed to show time in 12- or 24-hours HH:MM fo ...
- hdu 5236 Article 概率dp
Article Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=5236 ...
- QS世界大学排名_百度百科
QS世界大学排名_百度百科 莱斯特大学
- Phpstorm-svn配置
参考网站; http://blog.csdn.net/Knight_quan/article/details/51889476 1 打开PhpStorm,找到工具 VCS—>Checkout ...
- Xcode no visible @interface for XXX declares
出现如上面的错误, 是因为没有找到这个方法, 要自己写一个这样的方法 , 如果这是个类目的方法的话, 需要在Target->Linking->Other Linker Flags中添加- ...
- 《Windows驱动开发技术详解》之StartIO例程
内容中包含 base64string 图片造成字符过多,拒绝显示
- Wishbone总线从接口转Xilinx MIG (Spartan 6)
//*************************************************************************** // Copyright(c)2016, L ...
- java map 分析
java 8 对HashMap进行了优化, 当碰撞时使用TreeNode的二分方法查找数据: 但是当数据碰撞厉害的时候, table有很多浪费. table 大小等于size/factor, 当碰撞很 ...