Android組件化架構(gòu) —— 基礎(chǔ)(一) - 組件化與集成化

xwzz.jpg

什么是組件化?

回答這個(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配置信息不同

buildgradle配置區(qū)別.png

2、AndroidManifest.xml配置信息

清單文件配置區(qū)別.png

于是乎羔飞,你照貓畫虎的將user子模塊改成與App主模塊一致:

build.gradle修改.png
AndroidManifest.xml修改.png

經(jīng)過你的努力肺樟,user子模塊成功轉(zhuǎn)換為一個(gè)可執(zhí)行的Module,并完美運(yùn)行起來:

轉(zhuǎn)換成功.png
user運(yùn)行成功.png

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)完成了拓哺,看看效果:

自動化轉(zhuǎn)換

(諾GIF圖加載失敗,可點(diǎn)擊此處查看)

AndroidManifest.xml 問題

組件化模式轉(zhuǎn)換問題解決了脖母,但當(dāng)我們切換至集成化環(huán)境時(shí)(isComponent = false)士鸥,運(yùn)行主App會看到這樣現(xiàn)象:

兩個(gè)logo.png

沒錯(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文件坛善。
兩個(gè)清單文件.png
// 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蛲ぁ些膨!

AndroidManifest自動化

(諾GIF圖加載失敗,可點(diǎn)擊此處查看)

下篇钦铺,我們將探討組件間通訊方案有哪些订雾。

Android組件化架構(gòu) —— 基礎(chǔ)(二) - 組件間通訊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市矛洞,隨后出現(xiàn)的幾起案子洼哎,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件噩峦,死亡現(xiàn)場離奇詭異窑邦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)壕探,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門冈钦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人李请,你說我怎么就攤上這事瞧筛。” “怎么了导盅?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵较幌,是天一觀的道長。 經(jīng)常有香客問我白翻,道長乍炉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任滤馍,我火速辦了婚禮岛琼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘巢株。我一直安慰自己槐瑞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布阁苞。 她就那樣靜靜地躺著困檩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪那槽。 梳的紋絲不亂的頭發(fā)上悼沿,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機(jī)與錄音骚灸,去河邊找鬼糟趾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛逢唤,可吹牛的內(nèi)容都是我干的拉讯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼鳖藕,長吁一口氣:“原來是場噩夢啊……” “哼魔慷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起著恩,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤院尔,失蹤者是張志新(化名)和其女友劉穎蜻展,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邀摆,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纵顾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了栋盹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片施逾。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖例获,靈堂內(nèi)的尸體忽然破棺而出汉额,到底是詐尸還是另有隱情,我是刑警寧澤榨汤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布蠕搜,位于F島的核電站,受9級特大地震影響收壕,放射性物質(zhì)發(fā)生泄漏妓灌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一蜜宪、第九天 我趴在偏房一處隱蔽的房頂上張望虫埂。 院中可真熱鬧,春花似錦端壳、人聲如沸告丢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至岳颇,卻和暖如春照捡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背话侧。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工栗精, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞻鹏。 一個(gè)月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓悲立,卻偏偏與公主長得像,于是被迫代替她去往敵國和親新博。 傳聞我的和親對象是個(gè)殘疾皇子薪夕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容