使用 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:
- app1AdminDebug
- app1AdminRelease
- app1OfflineDebug
- app1OfflineRelease
- app1OnlineDebug
- app1OnlineRelease
- app2AdminDebug
- app2AdminRelease
- app2OfflineDebug
- app2OfflineRelease
- app2OnlineDebug
- app2OnlineRelease
是不是每個應用都有 3 個服務器版本,每個版本都有 debug 和 release 包。
動態配置 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:
sourceSets 文件目錄
然后在 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 ,可以和上面動態配置 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
來自:https://juejin.im/post/59648441f265da6c415f3078