代碼地址以下:
http://www.demodashi.com/demo/11297.htmlhtml
在文章 使用 Gradle 對應用進行個性化定製 中,咱們可以針對一個應用的正式服、測試服、超管服等其餘版本,進行個性化定製。
這一篇文章咱們來點大動做,讓你用一套代碼構建多個應用。android
需求:「將某個應用換一套皮膚、第三方帳號、後臺服務器,改個名字上線,而且之後的新功能同步進行更新」。git
當你遇到這樣的需求會怎麼作呢?api
是將項目複製一份,而後修改其中的內容,有新功能的時候再手動複製過來稍微修改一下 UI?服務器
或者能夠切換一個分支,在這個分支上修改相關的信息,每次開發完新功能,將代碼合併過來,再稍微修改新功能的 UI?微信
如今我來介紹使用 Gradle
的 flavorDimensions
,實現一份代碼構建多個應用。app
老規矩,先上完整的 Gradle
配置:dom
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 }
在上一篇文章的配置上進行了一些修改,同時保留上一篇文章裏全部的功能。ide
首先來看最重要的一個概念:佈局
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
包。
咱們要實現多應用,必須能安裝在同一臺手機上。因此不一樣應用之間的包名得不同。
在 APP
維度的 flavor
中設置不一樣的 applicationId
,就能夠實現修改應用包名。
app1{ applicationId 'com.imliujun.app1' } app2{ applicationId 'com.imliujun.app2' }
這樣配置後,app1
和 app2
就可以安裝在同一臺手機上,也能同時上傳應用商店。
有一點你們切記,AndroidManifest.xml
中的 package
不須要去修改,R 文件的路徑是根據這個 package
來生成的。若是對 package
進行修改,R 文件的路徑也會改變,全部引用到 R 文件的類都須要進行修改。
既然每一個 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
目錄,好比:
sourceSets
位置是 src/app1/
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大師代發,拒絕轉載,轉載須要做者受權