Android組件化實踐

一、背景


隨著APP版本不斷的迭代,新功能的不斷增加,業務也會變的越來越復雜,APP業務模塊的數量有可能還會繼續增加,而且每個模塊的代碼也變的越來越多,這樣發展下去單一工程下的APP架構勢必會影響開發效率,增加項目的維護成本,每個工程師都要熟悉如此之多的代碼,將很難進行多人協作開發,而且Android項目在編譯代碼的時候電腦會非常卡,又因為單一工程下代碼耦合嚴重,每修改一處代碼后都要重新編譯打包測試,導致非常耗時,所以必須要有更靈活的架構代替過去單一的工程架構。

二、簡介

那什么是組件化呢?組件化簡單概括就是把一個功能完整的 App 或模塊拆分成多個子模塊, 每個子模塊可以獨立編譯和運行, 也可以任意組合成另一個新的 App 或模塊, 每個模塊即不相互依賴但又可以相互交互,業務模塊之間的跳轉可以通過路由(Arouter)實現;業務模塊之間的通信可以通過消息(EventBus)來實現。

三、基礎搭建


1、組件框架圖
image.png
2、根據組件框架圖搭建的項目結構圖

image.png

項目中總共有五個 module ,包括 3 個業務模塊、一個基礎模塊和一個 APP 殼模塊。
在建好項目之后我們需要給 3 個 module 配置 “集成開發模式” 和 “組件開發模式” 的切換開關,可以在 gradle.properties 文件中定義變量 isModel ,isModel=false 代表是 “集成開發模式” , isModel=true 代表是 “組件開發模式” (注:每次修改isModel的值后一定要Sysn才會生效)。
image.png

1)APP 殼模塊

主要就是集成每一個模塊,最終打包成一個完整的 apk ,其中 gradle 做了如下配置,根據配置文件中的 isModel 字段來依賴不同的業務組件;

image.png

2)baselibs 模塊
主要負責封裝公共部分,如 MVP 架構、 BaseView 的封裝、網絡請求庫、圖片加載庫、工具類以及自定義控件等;

為了防止重復依賴,所有的第三方庫都放在這個模塊,業務模塊不做任何第三方依賴,只依賴于 baselibs 模塊。

baselibs 模塊的結構如下:


image.png

在 baselibs 模塊的 gradle 中引入的庫

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    configurations {
        all*.exclude group: 'com.android.support', module: 'support-v13'
    }
    testImplementation rootProject.ext.testDeps["junit"]
    androidTestImplementation rootProject.ext.testDeps["runner"]
    androidTestImplementation rootProject.ext.testDeps["espresso-core"]
    //leakCanary
    debugApi rootProject.ext.testDeps["leakcanary-debug"]
    releaseApi rootProject.ext.testDeps["leakcanary-release"]
    // Support庫
    api rootProject.ext.supportLibs
    // 網絡請求庫
    api rootProject.ext.networkLibs
    // RxJava2
    api rootProject.ext.rxJavaLibs
    // commonLibs
    api rootProject.ext.commonLibs
    kapt rootProject.ext.otherDeps["arouter-compiler"]
}

3)業務模塊(module_news、module_video、module_me)
每一個業務模塊在 “集成開發模式” 下以 library 的形式存在;在 “組件開發模式” 下以 application 的形式存在,可以單獨運行。
由于每個業務模塊的配置文件都差不多,下面就以 module_news 模塊為例;
以下是 module_news 模塊的 gradle 配置文件:

if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
android {
    if (isModule.toBoolean()) {
        applicationId "com.cxz.module.me"
    }
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    defaultConfig {
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode 1
        versionName "1.0"
    }
}
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    testImplementation rootProject.ext.testDeps["junit"]
    androidTestImplementation rootProject.ext.testDeps["runner"]
    androidTestImplementation rootProject.ext.testDeps["espresso-core"]
    implementation project(':baselibs')
    kapt rootProject.ext.otherDeps["arouter-compiler"]
}

4)配置文件 config.gradle ,對項目中的第三庫、 app 的版本等配置

ext {
    android = [
            compileSdkVersion: 28,
            buildToolsVersion: "28.0.3",
            minSdkVersion    : 16,
            targetSdkVersion : 27,
            versionCode      : 1,
            versionName      : "1.0.0"
    ]
    dependVersion = [
            androidSupportSdkVersion: "28.0.0",
            espressoSdkVersion      : "3.0.2",
            retrofitSdkVersion      : "2.4.0",
            glideSdkVersion         : "4.8.0",
            rxJava                  : "2.2.2",
            rxAndroid               : "2.1.0",
            rxKotlin                : "2.3.0",
            anko                    : "0.10.7"
    ]
    supportDeps = [
            "supportv4"        : "com.android.support:support-v4:${dependVersion.androidSupportSdkVersion}",
            "appcompatv7"      : "com.android.support:appcompat-v7:${dependVersion.androidSupportSdkVersion}",
            "cardview"         : "com.android.support:cardview-v7:${dependVersion.androidSupportSdkVersion}",
            "design"           : "com.android.support:design:${dependVersion.androidSupportSdkVersion}",
            "constraint-layout": "com.android.support.constraint:constraint-layout:1.1.3",
            "annotations"      : "com.android.support:support-annotations:${dependVersion.androidSupportSdkVersion}"
    ]
    retrofit = [
            "retrofit"                : "com.squareup.retrofit2:retrofit:${dependVersion.retrofitSdkVersion}",
            "retrofitConverterGson"   : "com.squareup.retrofit2:converter-gson:${dependVersion.retrofitSdkVersion}",
            "retrofitAdapterRxjava2"  : "com.squareup.retrofit2:adapter-rxjava2:${dependVersion.retrofitSdkVersion}",
            "okhttp3LoggerInterceptor": 'com.squareup.okhttp3:logging-interceptor:3.11.0',
            "retrofitConverterMoshi"  : 'com.squareup.retrofit2:converter-moshi:2.4.0',
            "retrofitKotlinMoshi"     : "com.squareup.moshi:moshi-kotlin:1.7.0"
    ]
    rxJava = [
            "rxJava"   : "io.reactivex.rxjava2:rxjava:${dependVersion.rxJava}",
            "rxAndroid": "io.reactivex.rxjava2:rxandroid:${dependVersion.rxAndroid}",
            "rxKotlin" : "io.reactivex.rxjava2:rxkotlin:${dependVersion.rxKotlin}",
            "anko"     : "org.jetbrains.anko:anko:${dependVersion.anko}"
    ]
    testDeps = [
            "junit"                    : 'junit:junit:4.12',
            "runner"                   : 'com.android.support.test:runner:1.0.2',
            "espresso-core"            : "com.android.support.test.espresso:espresso-core:${dependVersion.espressoSdkVersion}",
            "espresso-contrib"         : "com.android.support.test.espresso:espresso-contrib:${dependVersion.espressoSdkVersion}",
            "espresso-intents"         : "com.android.support.test.espresso:espresso-intents:${dependVersion.espressoSdkVersion}",
            "leakcanary-debug"         : 'com.squareup.leakcanary:leakcanary-android:1.6.1',
            "leakcanary-release"       : 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1',
            "leakcanary-debug-fragment": 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1',
            "debug-db"                 : 'com.amitshekhar.android:debug-db:1.0.4'
    ]
    commonDeps = [
            "multidex": 'com.android.support:multidex:1.0.3',
            "logger"  : 'com.orhanobut:logger:2.2.0',
            "glide"   : 'com.github.bumptech.glide:glide:4.8.0',
            "eventbus": 'org.greenrobot:eventbus:3.1.1',
            "spinkit" : 'com.github.ybq:Android-SpinKit:1.2.0',
            "arouter" : 'com.alibaba:arouter-api:1.4.0'
    ]
    otherDeps = [
            "arouter-compiler": 'com.alibaba:arouter-compiler:1.2.1'
    ]
    supportLibs = supportDeps.values()
    networkLibs = retrofit.values()
    rxJavaLibs = rxJava.values()
    commonLibs = commonDeps.values()
}

最后別忘記在工程的中 build.gradle 引入該配置文件

apply from: "config.gradle"

四、業務模塊之間交互

業務模塊之間的跳轉可以通過路由(Arouter)實現;業務模塊之間的通信可以通過消息(EventBus)來實現。

1、Arouter 實現業務模塊之間的跳轉

我們在之前已經依賴了 Arouter (詳細用法參照:github.com/alibaba/ARo…),用它來實現跳轉只需要以下兩步:

第一步

  • gradle 配置
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
    generateStubs = true
}
dependencies {
...
    kapt rootProject.ext.otherDeps["arouter-compiler"]
}

第二步

  • 需要指明目標頁面以及要帶的參數,然后在調用 navigation() 方法;


    image.png

第三步

  • 首先在 onCreate 方法調用 ARouter.getInstance().inject(this) 注入;
  • 然后要用 @Route 注解標注頁面,并在 path 變量中給頁面定義一個路徑;
  • 最后對于傳送過來的變量我們直接定義一個同名的字段用 @Autowired 變量標注,Arouter 會對該字段自動賦值


    image.png

2、EventBus 實現業務模塊之間的通訊

利用第三方如 EventBus 對消息進行管理。在 baselibs 組件中的 BaseActivity 、 BaseFragment 類做了對消息的簡單封裝,子類只需要重寫 useEventBus() 返回 true 即可對事件的注冊。

五、搭建過程中遇到的問題

1、AndroidManifest

我們知道 APP 在打包的時候最后會把所有的 AndroidManifest 進行合并,所以每個業務組件的 Activity 只需要在各自的模塊中注冊即可。

如果業務組件要單獨運行,則需要單獨的一個 AndroidManifest ,在 gradle 的 sourceSets 加載不同的 AndroidManifest 即可。


image.png

gradle 配置

android {
...
    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //集成開發模式下排除debug文件夾中的所有Java文件
                java {
                    exclude 'debug/**'
                }
                kotlin {
                    exclude 'debug/**'
                }
            }
        }
    }
...
}

注意:集成模式下的 AndroidManifest 不需要配置 Application ,組件模式下的 AndroidManifest 需要單獨配置 Application ,并且必須繼承 BaseApp 。

2、資源文件沖突的問題

不同業務組件里的資源文件的名稱可能相同,所以就可能出現資源文件沖突的問題,我們可以通過設置資源的前綴來防止資源文件的沖突。
這樣配置以后,如果我們在命名資源文件沒有加前綴的時候,編譯器就會提示我們沒加前綴。

image.png

gradle 配置,以 module_news 模塊為例

android {
...
    resourcePrefix "news_"
...
}

這樣配置以后,如果我們在命名資源文件沒有加前綴的時候,編譯器就會提示我們沒加前綴。

至此, Android 基本組件化框架已經搭建完成,如有錯誤之處還請指正。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • 編譯時間越來越長,時間=生命,我要救命。 項目框架 最開始項目只有一個app,項目結構很簡單,就是一個業務modu...
    展翅而飛閱讀 4,520評論 5 18
  • 前言 相信各位小伙伴們對組件化開發都不陌生了,本文只對我所理解和使用的組件化開發方案做一個總結,有不正確或者需要改...
    小豪丶ace閱讀 593評論 0 2
  • 模塊化方案實踐 為什么需要模塊化 在項目開發到一定階段,隨著功能需求越來越多,代碼結構越來越臃腫,維護也隨之越來越...
    希靈丶閱讀 5,325評論 2 16
  • 呂梓葦:11.20黃金昨夜為何跳水,梓葦對你說做單技巧 ——筆者呂梓葦金融分析師經過多年交易形成一套自己的交易系統...
    呂梓媯_68a7閱讀 171評論 0 0
  • 火車在向前奔跑, 山野在向后奔跑, 一路美妙的風景, 被風撕碎了。 藍天虛無漂渺, 白云絮絮擾擾, 樹林纏著山風一...
    曹煥甫閱讀 2,126評論 0 0