Android 組件化應(yīng)用

一個軟件系統(tǒng)的開發(fā)可能只需要2到3個月就能完成,而這個系統(tǒng)的迭代和維護時間可能達2到3年之久——《不記得哪本書上說的》
Android移動端項目經(jīng)過長時間的迭代和維護,代碼經(jīng)手不同的人,產(chǎn)生冗余和規(guī)范問題也是一件不可避免的事情,而單工程的架構(gòu)在面對這樣的情況更是心有余而力不足,組件化也正是針對此類場景衍生出來的技術(shù)手段


源碼地址

特點

組件化最明顯的兩個優(yōu)勢:代碼解耦并行開發(fā)。通過不同維度和應(yīng)用環(huán)境下進行不同程度的拆分,達到組件靈活配置,增加開發(fā)效率的目的。 所以細化來說,組件化就是根據(jù)功能和業(yè)務(wù)來拆分module,最后module組成模塊,而后模塊組裝成應(yīng)用。

組件化基礎(chǔ)架構(gòu).png

Demo地址

應(yīng)用入口:該層其實只是一個空殼,用來存放啟動頁面,同時依賴所有業(yè)務(wù)組件,從這里開始,便是全量應(yīng)用的起始位置。

應(yīng)用業(yè)務(wù)組件:根據(jù)不同業(yè)務(wù)橫向拆分出來的業(yè)務(wù)組件,任何一個業(yè)務(wù)組件都可以獨立出來成為一個應(yīng)用存在,有完整的應(yīng)用周期,這些組件是橫向拆分出來的,所以他們之間并不存在依賴關(guān)系。

通用業(yè)務(wù)功能組件:通用業(yè)務(wù)其實相當(dāng)用這類業(yè)務(wù)是從應(yīng)用業(yè)務(wù)中抽取出來的并集,從應(yīng)用上說,他屬于業(yè)務(wù),而針對應(yīng)用業(yè)務(wù)而言則更像是一種功能,好比登錄這種業(yè)務(wù)功能,不需要關(guān)心有沒有界面,當(dāng)中是怎樣的邏輯,只需要提供結(jié)果即可,如有必要,中間的過程也可以通過回調(diào)的方式向外部暴露出來。

支撐服務(wù)組件接口:通用業(yè)務(wù)組件對外聲明的功能接口抽取放到該層,可以說通用業(yè)務(wù)功能組件就是這層接口的實現(xiàn)者,同時考慮到應(yīng)用業(yè)務(wù)通用代碼的抽取,帶業(yè)務(wù)屬性的Base也可以放到該層。該層的接口抽取也可以理解這里是Module依賴的一種妥協(xié),當(dāng)同樣層級的兩個通用功能組件需要互相通信的時候,如果沒有抽取一層公共的接口層出來,就會造成你中有我我中有你的尷尬局面,破壞了組件化解耦項目的初衷。

功能支撐:項目中用到的諸如網(wǎng)絡(luò)請求,圖片加載,依賴的第三方和XXUtils等通用純Feature代碼就可以放到這里,是以上每層代碼的功能支撐,尤為重要,同時對部分通用View的封裝也可以放到這里供上層使用。

每層的橫向拆分模塊之間是沒有任何的依賴關(guān)系的,如果要做到相互之間的通信或是頁面的跳轉(zhuǎn),那么就需要提供一個路由來作為組件間通信的橋梁。

綜上所述,一個簡單的組件化工程結(jié)構(gòu)就出來了,說白了就是多Module開發(fā)而已


組件化工程目錄.png

代碼解耦

代碼解耦主要是從兩個方面,其一是公共代碼的抽取和歸納,其二是面向接口編程,接口下沉。

譬如說日期序列化方法,獲取字符串中的年月日,對外獲取日期字符串和format格式即可,??吹降腦XUtils便是常用的代碼抽取方式,上層只關(guān)心結(jié)果,不關(guān)心具體的實現(xiàn)邏輯這是一種簡單的減少代碼冗余方式。

public static Date serializeDate(String dateString, String dateFormat);

而代碼解耦其實與這種方式類似,只不過所調(diào)用的靜態(tài)方法變成了一個接口

public interface DateSerializable{
    void serializeDate(String dateString, String dateFormat);
}

上層去持有這個接口,而不是一個具體的類,通過實例化對應(yīng)的實現(xiàn)來獲得具有能力的實例來進行業(yè)務(wù),那么當(dāng)?shù)讓影l(fā)生了改變或是實現(xiàn)的時候,上層只需要實例化對應(yīng)的新實現(xiàn)類即可,如果把這層實例化也作為接口去作,那么上層完全不用改變就能擁抱變化,遵循設(shè)計原則去設(shè)計具體的應(yīng)用和功能代碼

依賴注入

繼續(xù)深入之后就是面向接口編程,上層與下層之間的通信以及下層提供能力的媒介以接口的形式存在,這樣后期實現(xiàn)的改變可以更換不同的實現(xiàn)類即可,上層幾乎不需要做任何的改動。對于能夠提供通用能力與業(yè)務(wù)沒有直接關(guān)系的Feature接口應(yīng)盡量下沉到底層。

而橫向的業(yè)務(wù)代碼或者功能實現(xiàn)可以進行依賴注入的方式來達到解耦的目。

依賴注入簡單示例
類比程序員ICoder,提供一個coding的能力

private interface ICoder {
    void coding();
}

而后提供一個工作者的類,由工作者來持有coding這項技能,如下:

private static class Worker{
    private ICoder mCoder;

    public Worker(ICoder coder){
        mCoder = coder;
    }

    public void working(){
        mCoder.coding();
    }
}

顯而易見,工作者內(nèi)部持有一個ICoder這樣的一個對象,而這個對象則是通過構(gòu)造傳遞進來,這樣的方式便能稱之為依賴注入Di(Dependency injection)

完善接下來的代碼

private static class Javaer implements ICoder{
        
    @Override
    public void coding(){
        System.out.println("i am java coder, code for java");
    }
}

private static class Pythoner implements ICoder {
    @Override
    public void coding(){
        System.out.println("i am python coder, code for python");
    }
}

創(chuàng)建兩個實現(xiàn)coding能力的類,分為Java開發(fā)和Python開發(fā),現(xiàn)在就相當(dāng)于公司有兩個程序員,他們分別是Java和Python,當(dāng)來活的時候,可以甩鍋到這兩位同僚身上。

public static void main(){
    Worker worker = new Worker(new Javaer());
    worker.working();
}

可以看到工作者持有的能力由外部賦值進去,工作者內(nèi)部需要持有對這層工作能力的接口。

上面依賴注入的方式就是一個控制反轉(zhuǎn)Ioc(Inversion of control)的例子:由程序內(nèi)部定制規(guī)則,外部傳遞遵循了內(nèi)部規(guī)則的實現(xiàn)。

結(jié)構(gòu)解耦

結(jié)構(gòu)的解耦其實一般針對應(yīng)用的整體業(yè)務(wù)而言進行的一個"分Module"操作,根據(jù)業(yè)務(wù)類型的橫向拆分,業(yè)務(wù)屬性的縱向拆分以及功能SDK下沉。
古老的開發(fā)套路一般都會將Feature相關(guān)的代碼單獨作為一個Module進行依賴,然后上層的應(yīng)用和業(yè)務(wù)寫到APP中去。


一般工程結(jié)構(gòu).png

這是最常見的操作,通過抽取常用的功能代碼,做上簡單的封裝,抽取到一個公共庫中,在編寫具體界面或者業(yè)務(wù)是可以直接拿來用。

而我們組件化的操作,則在此基礎(chǔ)上做一個更加細粒度的抽取,根據(jù)功能或是業(yè)務(wù)來進行橫向拆分縱向排序。

橫向拆分

橫向就是對類別不同的模塊做一個拆分,對應(yīng)的就是業(yè)務(wù)類模塊,功能類模塊,而具體細化下來就負責(zé)業(yè)務(wù)的
Business類模塊:businessA、businessB...
基礎(chǔ)業(yè)務(wù)功能模塊:login、share、welcome...
支撐功能類模塊:baseUI、commonLib...

縱向排序

而縱向排序就是對橫向拆分出來的模塊做一個依賴關(guān)系的排序


組件化縱向依賴關(guān)系.png

技術(shù)細節(jié)

多Module工程

根據(jù)上述理論對工程進行拆分
根據(jù)上面的框架圖,大致把一個基礎(chǔ)應(yīng)用項目拆分出下列幾個Module


組件化工程目錄.png

在此有一個小細節(jié),可以對基礎(chǔ)功能庫所依賴的三方做一個簡單的gradle管理

dependencies = [
            "appcompatv7"              : "com.android.support:appcompat-v7:${SUPPORT_LIB_VERSION}",
            "constraintlayout"         : "com.android.support.constraint:constraint-layout:${othersVersion.constraintlayout}",
            "recyclerview"             : "com.android.support:recyclerview-v7:${SUPPORT_LIB_VERSION}",
            "cardview"                 : "com.android.support:cardview-v7:${SUPPORT_LIB_VERSION}",
            "design"                   : "com.android.support:design:${SUPPORT_LIB_VERSION}",
        

            "glide"                    : "com.github.bumptech.glide:glide:${othersVersion.glideVersion}",
            "glideCompiler"            : "com.github.bumptech.glide:compiler:${othersVersion.glideVersion}",

            "rxjava"                   : "io.reactivex.rxjava2:rxjava:${othersVersion.rxjava2}",
            "rxandroid"                : "io.reactivex.rxjava2:rxandroid:${othersVersion.rxandroid}",
            "litepal"                  : "org.litepal.android:core:${othersVersion.litepal}",
            "retrofit"                 : "com.squareup.retrofit2:retrofit:${othersVersion.retrofit}",
            "convertergson"            : "com.squareup.retrofit2:converter-gson:${othersVersion.convertergson}",
            "adapterrxjava2"           : "com.squareup.retrofit2:adapter-rxjava2:${othersVersion.adapterrxjava2}",
            "converterScalars"         : "com.squareup.retrofit2:converter-scalars:${othersVersion.converterScalars}",
            "okHttpLoggingInterceptor" : "com.squareup.okhttp3:logging-interceptor:${othersVersion.okHttpLoggingInterceptor}",
            "eventbus"                 : "org.greenrobot:eventbus:${othersVersion.eventbus}",

            "leakcanaryAndroid"        : "com.squareup.leakcanary:leakcanary-android:${othersVersion.leakcanaryAndroid}",
            "leakcanaryAndroidNoOp"    : "com.squareup.leakcanary:leakcanary-android-no-op:${othersVersion.leakcanaryAndroidNoOp}",
            "leakcanarySupportFragment": "com.squareup.leakcanary:leakcanary-support-fragment:${othersVersion.leakcanarySupportFragment}",

            "arouterApi"               : "com.alibaba:arouter-api:${othersVersion.arouterApi}",
            "arouterCompiler"          : "com.alibaba:arouter-compiler:${othersVersion.arouterCompiler}",

    ]

圖中對依賴的三方進行了一個簡單的依賴,后期也可寫一個函數(shù)來將依賴進行升級,重點不在寫法,在于抽取。

  1. 在根目錄下建立一個xx.gradle文件
  2. 編寫對應(yīng)的依賴常量代碼
  3. 在build.gradle中引用
  4. 在對應(yīng)的文件中使用
    注意,如果想要在別的.gradle中使用聲明的這些常量,一定要在抽取的xx.gradle文件中將對應(yīng)的代碼塊用"ext"進行包裹
/**
     * 三方依賴庫
     */
    othersVersion = [
            glideVersion             : "4.8.0",

            rxjavaVersion            : "2.1.1",
            rxandroidVersion         : "2.0.1",

            constraintlayout         : "1.1.2",
            rxjava2                  : "2.1.1",
            rxandroid                : "2.0.1",
            recyclerview             : "27.1.1",
            litepal                  : "2.0.0",
            retrofit                 : "2.4.0",
            converterScalars         : "2.4.0",
            okHttpLoggingInterceptor : "3.8.0",
            convertergson            : "2.4.0",
            adapterrxjava2           : "2.4.0",
            eventbus                 : "3.1.1",

            leakcanaryAndroid        : "1.6.1",
            leakcanaryAndroidNoOp    : "1.6.1",
            leakcanarySupportFragment: "1.6.1",

            multiDex                 : "1.0.3",

            arouterApi               : "1.4.1",
            arouterCompiler          : "1.2.2",
            arouterRegister          : "1.0.2",

            buglyCrashReport         : "latest.release",
            buglyNativeCrashReport   : "latest.release"
    ]

在工程根目錄的build.gradle中加上如下代碼:

apply from: "config.gradle"

這樣在Module對應(yīng)的編譯腳本文件中就能正常引用了,如:

def ANDROID_VERSIONS = rootProject.ext.android
def OTHER_LIBRARY = rootProject.ext.dependencies
...
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    api OTHER_LIBRARY.appcompatv7
    api OTHER_LIBRARY.constraintlayout
    api OTHER_LIBRARY.recyclerview
    api OTHER_LIBRARY.cardview
    api OTHER_LIBRARY.retrofit
    api OTHER_LIBRARY.converterScalars
    api OTHER_LIBRARY.okHttpLoggingInterceptor
    api OTHER_LIBRARY.convertergson
    api OTHER_LIBRARY.adapterrxjava2
}

如此以后在查看或者更換依賴的時候也方便查看和維護,注意在進行依賴的過程中,因為依賴不同的三方,可能會出現(xiàn)重復(fù)依賴相同庫而版本不一致的情況,這里有兩種解決辦法,一種是在對應(yīng)依賴的三方中剔除對應(yīng)的pom依賴,如:

api('com.facebook.fresco:fresco:0.10.0') {
       exclude module: 'support-v4'
}

另外一種是強制依賴的相同庫的版本,如:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (requested.name.startsWith("support-") ||
                    requested.name.startsWith("animated") ||
                    requested.name.startsWith("cardview") ||
                    requested.name.startsWith("design") ||
                    requested.name.startsWith("gridlayout") ||
                    requested.name.startsWith("recyclerview") ||
                    requested.name.startsWith("transition") ||
                    requested.name.startsWith("appcompat")) {
                details.useVersion SUPPORT_LIB_VERSION
            } else if (requested.name.startsWith("multidex")) {
                details.useVersion OTHER_VERSION.multiDex
            }
        }
    }
}

組件路由

路由其實是組件化的核心組件,網(wǎng)上也有很多優(yōu)秀的開源庫,這里就直接使用阿里的開源庫ARouter,ARouter也是其中比較知名和穩(wěn)定的路由框架之一。

  1. 添加依賴和配置

    android {
        defaultConfig {
            ...
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [AROUTER_MODULE_NAME: project.getName()]
                }
            }
        }
    }
    
    dependencies {
        // 替換成最新版本, 需要注意的是api
        // 要與compiler匹配使用,均使用最新版可以保證兼容
        compile 'com.alibaba:arouter-api:x.x.x'
        annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
        ...
    }
    // 舊版本gradle插件(< 2.2),可以使用apt插件,配置方法見文末'其他#4'
    // Kotlin配置參考文末'其他#5'
    
  2. 添加注解

    // 在支持路由的頁面上添加注解(必選)
    // 這里的路徑需要注意的是至少需要有兩級,/xx/xx
    @Route(path = "/test/activity")
    public class YourActivity extend Activity {
        ...
    }
    
  3. 初始化SDK

    if (isDebug()) {           // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
        ARouter.openLog();     // 打印日志
        ARouter.openDebug();   // 開啟調(diào)試模式(如果在InstantRun模式下運行,必須開啟調(diào)試模式!線上版本需要關(guān)閉,否則有安全風(fēng)險)
    }
    ARouter.init(mApplication); // 盡可能早,推薦在Application中初始化
    
  4. 發(fā)起路由操作

    // 1. 應(yīng)用內(nèi)簡單的跳轉(zhuǎn)(通過URL跳轉(zhuǎn)在'進階用法'中)
    ARouter.getInstance().build("/test/activity").navigation();
    
    // 2. 跳轉(zhuǎn)并攜帶參數(shù)
    ARouter.getInstance().build("/test/1")
                .withLong("key1", 666L)
                .withString("key3", "888")
                .withObject("key4", new Test("Jack", "Rose"))
                .navigation();
    
  5. 添加混淆規(guī)則(如果使用了Proguard)

    -keep public class com.alibaba.android.arouter.routes.**{*;}
    -keep public class com.alibaba.android.arouter.facade.**{*;}
    -keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;}
    
    # 如果使用了 byType 的方式獲取 Service,需添加下面規(guī)則,保護接口
    -keep interface * implements com.alibaba.android.arouter.facade.template.IProvider
    
    # 如果使用了 單類注入,即不定義接口實現(xiàn) IProvider,需添加下面規(guī)則,保護實現(xiàn)
    # -keep class * implements com.alibaba.android.arouter.facade.template.IProvider
    
  6. 使用 Gradle 插件實現(xiàn)路由表的自動加載 (可選)

    apply plugin: 'com.alibaba.arouter'
    
    buildscript {
        repositories {
            jcenter()
        }
    
        dependencies {
            classpath "com.alibaba:arouter-register:?"
        }
    }
    

    可選使用,通過 ARouter 提供的注冊插件進行路由表的自動加載(power by AutoRegister), 默認通過掃描 dex 的方式
    進行加載通過 gradle 插件進行自動注冊可以縮短初始化時間解決應(yīng)用加固導(dǎo)致無法直接訪問
    dex 文件,初始化失敗的問題,需要注意的是,該插件必須搭配 api 1.3.0 以上版本使用!

  7. 使用 IDE 插件導(dǎo)航到目標(biāo)類 (可選)
    在 Android Studio 插件市場中搜索 ARouter Helper, 或者直接下載文檔上方 最新版本 中列出的 arouter-idea-plugin zip 安裝包手動安裝,安裝后
    插件無任何設(shè)置,可以在跳轉(zhuǎn)代碼的行首找到一個圖標(biāo) (

    navigation
    )
    點擊該圖標(biāo),即可跳轉(zhuǎn)到標(biāo)識了代碼中路徑的目標(biāo)類

單獨調(diào)試

當(dāng)工程被拆分為組件化的時候,那么Module的單獨調(diào)試就顯得尤為重要,無論是對問題的跟蹤還是業(yè)務(wù)線的并行開發(fā),都需要工程具備單獨運行調(diào)試的能力。這里單獨調(diào)試同樣是對gradle的操作,通過對編譯腳本的編寫來達到組件單獨運行的目的
1.對需要單獨運行的Module抽取變量進行記錄

 /**
     * 歡迎組件
     */
    welcomeLibRunAlone = false

    /**
     * 分享組件
     */
    shareLibRunAlone = false

    /**
     * 主業(yè)務(wù)組件
     */
    mainBusinessLibRunAlone = false

    /**
     * 登錄組件
     */
    loginLibRunAlone = false
  1. 在對應(yīng)Module的編譯腳本文件中添加判斷Module是以Lib形式依賴還是以App方式進行依賴,如下代碼
def runAlone = rootProject.ext.loginLibRunAlone.toBoolean()

if (runAlone) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

當(dāng)進行到這里,其實就可以讓Module單獨運行起來,同時添加Application中一些必要的元素,清單文件Manifest.xml文件,但是這個xml文件是文件在單獨運行的過程中所需要的,所以這里要放到一個單獨的目錄下


組件化單獨運行目錄結(jié)構(gòu).png

同時在此基礎(chǔ)上通過編譯腳本配置單獨運行時獲取的Android相關(guān)文件

sourceSets {
        main {
            if (runAlone) {
                manifest.srcFile 'src/main/runAlone/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java{
                    exclude 'src/main/runAlone/*'
                }
            }
        }
    }

根據(jù)以上配置,就可以使得登錄模塊單獨運行起來,調(diào)試起來非常方便

整體調(diào)試

根據(jù)上述方案,app殼其實只需要依賴業(yè)務(wù)組件即可

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation project(':mainbusinesslib')
}

業(yè)務(wù)組件在根據(jù)調(diào)試變量對相應(yīng)的基礎(chǔ)組件進行依賴

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    api project(':baseuilib')
    annotationProcessor OTHER_LIBRARY.arouterCompiler
    if(!shareAlone){
        api project(':sharelib')
    }
    if(!welcomeAlone){
        api project(':welcomelib')
    }
    if(!loginAlone){
        api project(':loginlib')
    }
}

編譯優(yōu)化

這里的編譯優(yōu)化是針對gradle編譯而言,其一是gradle在編譯時的配置,如優(yōu)化不檢查png圖片的合法性等等

其二是固化底層代碼,只編譯業(yè)務(wù)線代碼,所謂固化其實是對開發(fā)不相關(guān)的代碼進行aar依賴,這樣在編譯的時候就不需要去編譯這些庫中的代碼,大大節(jié)省編譯時間,增加開發(fā)效率

def dependencyMode = "aar"
...
    commonLibVersino = "1.0.0"
    loginLibVersino = "1.0.0"
    shareLibVersino = "1.0.0"
    welcomeLibVersino = "1.0.0"
    switchs = [
            "CommonLib"  : dependencyMode == "aar",
            "ShareModule": dependencyMode == "",
            "LoginModule": dependencyMode == "",
            "WelcomeLib" : dependencyMode == "",
    ]
...
dependencies = [
           ...
            "commonlib" : switchs['CommonLib'] ? "com.done.commonlib:CommonLib:" + "$commonLibVersino" : findProject(':commonlib')

    ]

同時在setting.gradle中編寫一下代碼

...
include switchs['CommonLib'] ? '' : 'commonlib'

綜上,一個簡單的組件化架構(gòu)就搭建完畢了

后期會整理一下的相關(guān)的代碼發(fā)布到github上哈歡迎大家進群交流哈群號:929891705
Demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容