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

1、背景

當咱們的項目變得愈來愈大,代碼變得愈來愈臃腫,耦合會愈來愈多,編譯速度愈來愈慢,開發效率也會變得愈來愈低,怎麼辦?這個時候咱們就須要對舊項目進行重構,便是模塊的拆分,官方的說法就是組件化。java

2、簡介

那什麼是組件化呢?其基本理念是:把經常使用的功能、控件、基礎類、第三方庫、權限等公共部分抽離封裝,咱們稱之爲基礎組件(baselibs);把業務分紅 N 個模塊進行獨立的管理,每個模塊咱們稱之爲業務組件;而全部的業務組件都須要依賴於封裝的基礎組件,業務組件之間不作依賴,這樣的目的是爲了讓每個業務模塊都能單獨運行。而在 APP 層對整個項目的模塊進行封裝。react

業務模塊之間的跳轉能夠經過路由(Arouter)實現;業務模塊之間的通訊能夠經過消息(EventBus)來實現。android

3、基礎搭建

一、組件框架圖

二、根據組件框架圖搭建的項目結構圖

項目結構圖

三、接下來介紹每一個模塊

項目中總共有五個 module ,包括 3 個業務模塊、一個基礎模塊和一個 APP 殼模塊。git

在建好項目以後咱們須要給 3 個 module 配置 「集成開發模式」 和 「組件開發模式」 的切換開關,能夠在 gradle.properties 文件中定義變量 isModelisModel=false 表明是 「集成開發模式」 , isModel=true 表明是 「組件開發模式」 (注:每次修改isModel的值後必定要Sysn纔會生效)。github

1)APP 殼模塊api

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

2)baselibs 模塊網絡

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

爲了防止重複依賴,全部的第三方庫都放在這個模塊,業務模塊不作任何第三方依賴,只依賴於 baselibs 模塊。app

baselibs 模塊的結構以下:

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"
複製代碼

4、業務模塊之間交互

業務模塊之間的跳轉能夠經過路由(Arouter)實現;業務模塊之間的通訊能夠經過消息(EventBus)來實現。

一、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() 方法;

第三步

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

二、EventBus 實現業務模塊之間的通信

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

5、搭建過程當中遇到的問題

一、AndroidManifest

咱們知道 APP 在打包的時候最後會把全部的 AndroidManifest 進行合併,因此每一個業務組件的 Activity 只須要在各自的模塊中註冊便可。

若是業務組件要單獨運行,則須要單獨的一個 AndroidManifest ,在 gradlesourceSets 加載不一樣的 AndroidManifest 便可。

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 。

二、資源文件衝突的問題

不一樣業務組件裏的資源文件的名稱可能相同,因此就可能出現資源文件衝突的問題,咱們能夠經過設置資源的前綴來防止資源文件的衝突。

gradle 配置,以 module_news 模塊爲例

android {
...
    resourcePrefix "news_"
...
}
複製代碼

這樣配置之後,若是咱們在命名資源文件沒有加前綴的時候,編譯器就會提示咱們沒加前綴。

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

5、最後

完整的項目地址:github.com/iceCola7/An…

相關文章
相關標籤/搜索