基於 MVP 的 Android 元件化開發框架實踐

iceCola7發表於2019-02-26

一、背景

當我們的專案變得越來越大,程式碼變得越來越臃腫,耦合會越來越多,編譯速度越來越慢,開發效率也會變得越來越低,怎麼辦?這個時候我們就需要對舊專案進行重構,即是模組的拆分,官方的說法就是元件化。

二、簡介

那什麼是元件化呢?其基本理念是:把常用的功能、控制元件、基礎類、第三方庫、許可權等公共部分抽離封裝,我們稱之為基礎元件(baselibs);把業務分成 N 個模組進行獨立的管理,每一個模組我們稱之為業務元件;而所有的業務元件都需要依賴於封裝的基礎元件,業務元件之間不做依賴,這樣的目的是為了讓每一個業務模組都能單獨執行。而在 APP 層對整個專案的模組進行封裝。

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

三、基礎搭建

1、元件框架圖

基於 MVP 的 Android 元件化開發框架實踐

2、根據元件框架圖搭建的專案結構圖

專案結構圖

3、接下來介紹每個模組

專案中總共有五個 module ,包括 3 個業務模組、一個基礎模組和一個 APP 殼模組。

在建好專案之後我們需要給 3 個 module 配置 “整合開發模式” 和 “元件開發模式” 的切換開關,可以在 gradle.properties 檔案中定義變數 isModelisModel=false 代表是 “整合開發模式” , isModel=true 代表是 “元件開發模式” (注:每次修改isModel的值後一定要Sysn才會生效)。

基於 MVP 的 Android 元件化開發框架實踐

1)APP 殼模組

主要就是整合每一個模組,最終打包成一個完整的 apk ,其中 gradle 做了如下配置,根據配置檔案中的 isModel 欄位來依賴不同的業務元件;

基於 MVP 的 Android 元件化開發框架實踐

2)baselibs 模組

主要負責封裝公共部分,如 MVP 架構、 BaseView 的封裝、網路請求庫、圖片載入庫、工具類以及自定義控制元件等;

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

baselibs 模組的結構如下:

基於 MVP 的 Android 元件化開發框架實踐

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 配置
kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
    generateStubs = true
}
dependencies {
...
    kapt rootProject.ext.otherDeps["arouter-compiler"]
}
複製程式碼

第二步

  • 需要指明目標頁面以及要帶的引數,然後在呼叫 navigation() 方法;

基於 MVP 的 Android 元件化開發框架實踐

第三步

  • 首先在 onCreate 方法呼叫 ARouter.getInstance().inject(this) 注入;
  • 然後要用 @Route 註解標註頁面,並在 path 變數中給頁面定義一個路徑;
  • 最後對於傳送過來的變數我們直接定義一個同名的欄位用 @Autowired 變數標註,Arouter 會對該欄位自動賦值

基於 MVP 的 Android 元件化開發框架實踐

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

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

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

1、AndroidManifest

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

如果業務元件要單獨執行,則需要單獨的一個 AndroidManifest ,在 gradlesourceSets 載入不同的 AndroidManifest 即可。

基於 MVP 的 Android 元件化開發框架實踐

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、資原始檔衝突的問題

不同業務元件裡的資原始檔的名稱可能相同,所以就可能出現資原始檔衝突的問題,我們可以通過設定資源的字首來防止資原始檔的衝突。

基於 MVP 的 Android 元件化開發框架實踐

gradle 配置,以 module_news 模組為例

android {
...
    resourcePrefix "news_"
...
}
複製程式碼

這樣配置以後,如果我們在命名資原始檔沒有加字首的時候,編譯器就會提示我們沒加字首。

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

五、最後

完整的專案地址:github.com/iceCola7/An…

相關文章