什么是組件化?
回答這個(gè)問題前侦高,我們先假設(shè)一個(gè)場景:
隨著公司業(yè)務(wù)越來越好嫉柴,原先的App團(tuán)隊(duì)開始劃分為多個(gè)業(yè)務(wù)小組,例如:用戶組(負(fù)責(zé)維護(hù)用戶信息相關(guān)業(yè)務(wù)奉呛,如:登錄计螺、注冊等)、商城組(負(fù)責(zé)維護(hù)商城訂單相關(guān)業(yè)務(wù)瞧壮,如:訂單列表登馒、下單、訂單詳情等)...
某日下午咆槽,用戶組小A氣勢洶洶來到商城組小B面前陈轿。
用戶組小A:你怎么修改了我用戶詳情接收的參數(shù)數(shù)據(jù)?
商城組小B:你那寫的什么玩意秦忿,調(diào)你的詳情傳遞訂單用戶信息都找不到入口麦射,當(dāng)然得改。
用戶組小A:那你也不能擅自修改啊灯谣,現(xiàn)在其他模塊一調(diào)就蹦法褥。
商城組小B:項(xiàng)目急著上線,加班到大半夜酬屉,哪有時(shí)間找你半等!
于是,倆人撕打在了一起呐萨,你作為移動端大哥杀饵,目睹眼前一切,腦海中不經(jīng)飄過一個(gè)念頭:“本是同根生谬擦,相煎何太急”切距。
!2以丁谜悟!~不對话肖,不對,以下才是你真實(shí)想法:
有沒有方法能完全隔離各部門之間的業(yè)務(wù)模塊葡幸,通訊之間通過一定協(xié)議規(guī)則來約束最筒,業(yè)務(wù)部門開發(fā)過程中只專注自己的模塊,從物理上杜絕跨業(yè)務(wù)修改代碼蔚叨?
Library床蜘?對了,能不能讓所有的業(yè)務(wù)子模塊變?yōu)長ibrary蔑水,提供給App主模塊引用邢锯,各部門只負(fù)責(zé)編寫自己的業(yè)務(wù)Library?
但Library不能像App主模塊一樣自主編譯調(diào)試搀别,如果在App主模塊里開發(fā)完再抽取成Library顯然是不可取的丹擎,能不能在開發(fā)過程中讓業(yè)務(wù)部門的Library是個(gè)自主運(yùn)行的Module,打包上線時(shí)再轉(zhuǎn)換為供App主模塊引用的Library歇父?
帶著疑問鸥鹉,你打開Android Studio創(chuàng)建一個(gè)app主模塊,又建了個(gè)供其依賴的user模塊的Library庶骄,查看它們之間有什么不同毁渗。
Android 主Module 與 Library配置上有什么區(qū)別?
經(jīng)過你細(xì)心查看单刁,發(fā)現(xiàn)以下兩個(gè)文件有所不同:
1灸异、build.gradle配置信息不同
2、AndroidManifest.xml配置信息
于是乎羔飞,你照貓畫虎的將user子模塊改成與App主模塊一致:
經(jīng)過你的努力肺樟,user子模塊成功轉(zhuǎn)換為一個(gè)可執(zhí)行的Module,并完美運(yùn)行起來:
OK , 到這你已經(jīng)手動完成Library轉(zhuǎn)換為可執(zhí)行Module的整個(gè)過程逻淌,反過來將一個(gè)Module轉(zhuǎn)換為Library么伯,相信你也手到擒來。
不妨總結(jié)下卡儒,將Library轉(zhuǎn)換為可執(zhí)行Module的過程田柔,稱之為“組件化”過程,轉(zhuǎn)換為組件后骨望,業(yè)務(wù)部門對其進(jìn)行開發(fā)硬爆,開發(fā)完畢再轉(zhuǎn)換為Library供app主模塊引入,最終打出完整的apk包擎鸠,這個(gè)過程稱之為“集成化”過程缀磕。
如何做到自動化轉(zhuǎn)換毫炉?
顯然泵殴,如果開發(fā)中手動去做轉(zhuǎn)換,這樣的體驗(yàn)很糟糕讯柔,且極易出錯(cuò)缨历,不妨交給Gradle試試本谜。
以上面為例纹冤,app作為主模塊径荔,user作為子模塊,我們先用Gradle將這兩個(gè)模塊涉及到的依賴以及版本信息統(tǒng)一管理起來颠黎,歩奏大致如下:
- 1另锋、在項(xiàng)目根目錄創(chuàng)建config.gradle文件滞项;
- 2狭归、配置版本、依賴庫相關(guān)信息文判;
- 3过椎、并在項(xiàng)目根目錄的build.gradle中將其導(dǎo)入。
(相關(guān)代碼已貼在下方)
// config.gradle文件內(nèi)容:
ext {
// true 組件化環(huán)境戏仓,將所有業(yè)務(wù)Library組件化為可執(zhí)行Module疚宇,供開發(fā)人員開發(fā)
// false 集成環(huán)境,將所有可執(zhí)行Module集成化為Library赏殃,打包到App主模塊里
isComponent = false
kotlin_version = "1.3.72"
ktx_version = "1.3.2"
appcompat_version = "1.2.0"
material_version = "1.2.1"
constraintlayout_version = "2.0.4"
kotlin_mvp_version = "1.2.1"
//App編譯環(huán)境 字典配置
application = [
compileSdkVersion: 30,
buildToolsVersion: "30.0.2",
minSdkVersion : 16,
targetSdkVersion : 30
]
//各模塊AppId 字典配置
appId = [
app : "com.ljb.myapp",
user: "com.ljb.myapp.user"
]
//各模塊引入的第三方公共庫 字典配置
dependenciesImport = [
kotlin_stdlib : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version",
ktx : "androidx.core:core-ktx:$ktx_version",
appcompat : "androidx.appcompat:appcompat:$appcompat_version",
material : "com.google.android.material:material:$material_version",
constraintlayout: "androidx.constraintlayout:constraintlayout:$constraintlayout_version",
]
}
// 項(xiàng)目根目錄build.gradle導(dǎo)入config.gradle:
apply from: "config.gradle"
buildscript {
repositories {
jcenter()
google()
}
...
每個(gè)模塊的build.gradle配置完后敷待,大致如下(以app主模塊為例):
//app主模塊build.gradle中的配置
plugins {
id 'com.android.application'
id 'kotlin-android'
}
def appId = rootProject.ext.appId
def application = rootProject.ext.application
def dependenciesImport = rootProject.ext.dependenciesImport
def isRelease = rootProject.ext.isRelease
android {
compileSdkVersion application.compileSdkVersion
buildToolsVersion application.buildToolsVersion
defaultConfig {
applicationId appId.app
minSdkVersion application.minSdkVersion
targetSdkVersion application.targetSdkVersion
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
//將當(dāng)前構(gòu)建環(huán)境狀態(tài)寫入 BuildConfig 文件中
buildConfigField("boolean", "isComponent", String.valueOf(isRelease))
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
dependenciesImport.each { k, v -> implementation(v) }
}
到此,基本的配置已經(jīng)完成仁热,細(xì)心的你可能已將發(fā)現(xiàn)在config.gradle中定義了一個(gè)isComponent字段:
// true 組件化環(huán)境榜揖,將所有業(yè)務(wù)Library組件化為可執(zhí)行Module,供開發(fā)人員開發(fā)
// false 集成環(huán)境抗蠢,將所有可執(zhí)行Module集成化為Library举哟,打包到App主模塊里
isComponent = false
通過修改這個(gè)字段,我們希望當(dāng)它為true時(shí)迅矛,表示組件開發(fā)環(huán)境妨猩,將所有Library組件化為可執(zhí)行Module,供開發(fā)人員開發(fā)秽褒;當(dāng)它為false時(shí)壶硅,表示集成發(fā)布環(huán)境,將所有可執(zhí)行Module集成化為Library销斟,打包到App主模塊里森瘪。
前面我們也分析了,對于Library來說票堵,轉(zhuǎn)換為可執(zhí)行Module扼睬,在其build.gradle中我們需要修改兩處:
- 1、將‘com.android.library’ 改為 ‘com.android.application’
- 2、添加 applicationId
現(xiàn)在窗宇,有了isComponent 字段后措伐,在user子模塊的build.gradle通過代碼實(shí)現(xiàn)這個(gè)過程,如下:(重點(diǎn)看注釋部分)
// 1军俊、取出isComponent字段
def isComponent = rootProject.ext.isComponent
// 2侥加、根據(jù)isComponent字段,來確定當(dāng)前是集成化 還是組件化
if (isComponent) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
def appId = rootProject.ext.appId
def application = rootProject.ext.application
def dependenciesImport = rootProject.ext.dependenciesImport
def version_code = rootProject.ext.versionCode
def version_name = rootProject.ext.versionName
android {
compileSdkVersion application.compileSdkVersion
buildToolsVersion application.buildToolsVersion
defaultConfig {
// 3粪躬、如果當(dāng)前是組件化担败,那么就需要 applicationId
if (isComponent) {
applicationId appId.user
}
minSdkVersion application.minSdkVersion
targetSdkVersion application.targetSdkVersion
versionCode version_code.user
versionName version_name.user
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
dependenciesImport.each { k, v -> implementation(v) }
}
而對于app主模塊來說,如果當(dāng)前是集成化镰官,還需以Library的形式將子模塊依賴進(jìn)來提前,所以還需修改app主模塊的build.gradle文件,如下:(重點(diǎn)看注釋部分)
plugins {
id 'com.android.application'
id 'kotlin-android'
}
//1泳唠、取出isComponent字段
def isComponent = rootProject.ext.isComponent
def appId = rootProject.ext.appId
def application = rootProject.ext.application
def dependenciesImport = rootProject.ext.dependenciesImport
def version_code = rootProject.ext.versionCode
def version_name = rootProject.ext.versionName
android {
compileSdkVersion application.compileSdkVersion
buildToolsVersion application.buildToolsVersion
defaultConfig {
applicationId appId.app
minSdkVersion application.minSdkVersion
targetSdkVersion application.targetSdkVersion
versionCode version_code.app
versionName version_name.app
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
dependenciesImport.each { k, v -> implementation(v) }
//2狈网、當(dāng)前是發(fā)布環(huán)境,那么需要引入其它子模塊的Library
if (!isComponent) {
implementation project(path: ':user')
}
}
到此笨腥,整個(gè)自動化轉(zhuǎn)換的配置就已經(jīng)完成了拓哺,看看效果:
AndroidManifest.xml 問題
組件化模式轉(zhuǎn)換問題解決了脖母,但當(dāng)我們切換至集成化環(huán)境時(shí)(isComponent = false)士鸥,運(yùn)行主App會看到這樣現(xiàn)象:
沒錯(cuò),手機(jī)屏幕上出現(xiàn)了兩個(gè)APP入口谆级?
這是因?yàn)橹拔覀兪謩訉ser子模塊進(jìn)行組件化過程中烤礁,對其AndroidManifest.xml中application以及UserMainActivity配置了logo和Launch入口;而在集成化過程中哨苛,各模塊AndroidManifest.xml合并為按一個(gè)文件鸽凶,最終導(dǎo)致產(chǎn)生了兩個(gè)程序入口。
顯然建峭,在組件環(huán)境下子模塊是需要Launch入口的玻侥,而集成環(huán)境下又不需要。
最簡單的方式呢亿蒸,就是使用兩個(gè)AndroidManifest.xml凑兰,一個(gè)有入口,一個(gè)沒有边锁;一個(gè)給組建環(huán)境使用姑食,一個(gè)給集成環(huán)境使用。
那么茅坛,按照這個(gè)思路音半,我的實(shí)現(xiàn)方案如下:
- 1则拷、在user子模塊的main文件夾下新建_ReleaseManifest文件夾;
- 2曹鸠、拷貝一份AndroidManifest.xml到該文件夾下煌茬,并刪除logo以及Launch入口相關(guān)代碼;
- 3彻桃、在子模塊build.gradle中根據(jù)isComponent字段來指定對應(yīng)的AndroidManifest.xml文件坛善。
// user模塊 build.gradle
android {
...
sourceSets {
main {
// 組建環(huán)境與集成環(huán)境時(shí)使用不同的AndroidManifest.xml文件
if (isComponent) {
manifest.srcFile 'src/main/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/_ReleaseManifest/AndroidManifest.xml'
}
}
}
...
}
測試下,嗯邻眷,沒問題眠屎。但仔細(xì)想想,AndroidManifest.xml文件是我們開發(fā)過程中需要經(jīng)常修改的文件肆饶,而現(xiàn)在就需要修改兩次改衩,或者說每次切換至集成環(huán)境都需同步一次。這樣未免過于繁瑣抖拴,而且手動同步也極易出錯(cuò)燎字,怎么辦腥椒?
交給Gradle阿宅!在編譯期,通過腳本來實(shí)現(xiàn)拷貝及刪除工作笼蛛,比起人工往往更安全且高效洒放,相關(guān)的代碼實(shí)現(xiàn)我也貼在了下方,核心思路還是和上面一樣滨砍,只需:
- 1往湿、在項(xiàng)目根目錄創(chuàng)建manifestRelease.gradle文件,并粘貼下方代碼:
// manifestRelease.gradle 文件內(nèi)容
import groovy.xml.XmlUtil
def log(String moduleName, String info) {
println("<$moduleName> ===> $info")
}
def manifestRelease(String moduleName) {
//==================Start (集成化AndroidManifest)=====================
//找到這個(gè)模塊的路徑
String originDir = project(moduleName).projectDir
//copy AndroidManifest
def releaseManifestDir = "${originDir}/src/main/_ReleaseManifest"
copy() {
from "${originDir}/src/main/AndroidManifest.xml"
into releaseManifestDir
}
//刪除不需要的屬性
def releaseManifestFile = "${releaseManifestDir}/AndroidManifest.xml"
def parser = new XmlParser(false, false)
def releaseManifestXml = parser.parse(releaseManifestFile)
//刪除application中的屬性
releaseManifestXml.application.each { application ->
def keys = application.attributes().keySet()
def newKeyList = new ArrayList(keys)
newKeyList.forEach {
def attrStr = it.toString()
// application 需要的屬性保留在這里
def filter = (attrStr.contains('android:allowBackup')
|| attrStr.contains('android:supportsRtl')
|| attrStr.contains('android:theme'))
if (!filter) {
log(moduleName, "remove application attributes :: ${it}")
application.attributes().remove(it)
}
}
application.attributes().keySet().forEach {
log(moduleName, "has application attributes :: ${it}")
}
//刪除 LAUNCHER <intent-filter>
def categoryList = releaseManifestXml.application.activity.'intent-filter'.category
log(moduleName, categoryList.toString())
categoryList.forEach { category ->
def categoryName = category.attributes().get('android:name')
if (categoryName == 'android.intent.category.LAUNCHER') {
def intent_filter = category.parent()
if (intent_filter.name() == 'intent-filter') {
def delResult = intent_filter.parent().remove(intent_filter)
log(moduleName, "del android.intent.category.LAUNCHER for intent-filter :: $delResult")
}
}
}
//保存
PrintWriter pw = new PrintWriter(releaseManifestFile, ("UTF-8"))
pw.write(XmlUtil.serialize(releaseManifestXml))//用XmlUtil.serialize方法,將String改為xml格式
pw.close()
}
//==================End (集成化AndroidManifest)=====================
}
ext {
manifestRelease = this.&manifestRelease
}
- 2惋戏、使用時(shí)领追,和導(dǎo)入config.gradle類似,首先在項(xiàng)目根目錄的build.gradle中導(dǎo)入腳本:
// 根目錄中的build.gradle文件內(nèi)容
apply from: "config.gradle"
// 導(dǎo)入我們編寫的manifestRelease腳本
apply from: "manifestRelease.gradle"
buildscript {
...
- 3响逢、最后绒窑,在子模塊的build.gradle中調(diào)用腳本函數(shù)即可:
// user子模塊的build.gradle文件內(nèi)容
def isComponent = rootProject.ext.isComponent
if (isComponent) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
...
// 調(diào)用清單文件處理函數(shù)
rootProject.ext.manifestRelease(project.name)
android {
compileSdkVersion application.compileSdkVersion
...
來看看最后的效果吧!L蛲ぁ些膨!
下篇钦铺,我們將探討組件間通訊方案有哪些订雾。