Android 組件化,從入門到不可自拔

前言

組件化技術(shù),在 Android 開發(fā)中有著舉足輕重的作用。

隨著時間推移,軟件項目很多都會變得越來越龐雜。此時,采用組件化技術(shù),對項目進行改造,是一種較優(yōu)的方案。

談?wù)勀K化

要聊組件化,慣例是要談?wù)勀K化的,畢竟它與組件化確實有一些相同點,在組件化的項目中它也會與組件化發(fā)生關(guān)聯(lián)。

什么是模塊化

模塊化開發(fā),是每個開發(fā)者都熟悉的。

即將常用的UI、網(wǎng)絡(luò)請求、數(shù)據(jù)庫操作、第三方庫的使用等公共部分抽離封裝成基礎(chǔ)模塊,或者將大的業(yè)務(wù)上拆分為多個小的業(yè)務(wù)模塊,這些業(yè)務(wù)模塊又依賴于公共基礎(chǔ)模塊的開發(fā)方式。

更宏觀上,又會將這些不同的模塊組合為一個整體,打包成一個完成的項目。

模塊化的好處

模塊化有哪些好處呢?

復用

首先,基礎(chǔ)模塊,可為業(yè)務(wù)模塊所復用;

其次,子業(yè)務(wù)模塊,可為父業(yè)務(wù)模塊,甚至不同的項目所復用。

解耦

降低模塊間的耦合,避免出現(xiàn)一處代碼修改,牽一發(fā)而動全身的尷尬局面。

協(xié)同開發(fā)

項目越來越大,團隊人數(shù)越來越多,模塊化開發(fā)可在盡量解耦的情況下,使不同的開發(fā)人員專注于自己負責的業(yè)務(wù),同步開發(fā),顯著提供開發(fā)效率。

模塊化的弊端

那,模塊化開發(fā)有沒有什么弊端呢?

有。

任憑模塊化做得多么好,還是跳不出是組合在單一項目下的。隨著項目的發(fā)展與迭代,模塊化開發(fā)漸漸顯現(xiàn)了以下的問題:

項目代碼量越來越大

每次的編譯速度越來越慢,哪怕幾行代碼的修改,都需要花費好幾分鐘的時間,等著編譯器編譯運行結(jié)束后,才能查看代碼的執(zhí)行結(jié)果,這極大的降低了開發(fā)效率;

業(yè)務(wù)模塊越來越多

不可避免地產(chǎn)生越來越多且復雜的耦合,哪怕一次小的功能更新,也需要對修改代碼耦合的模塊進行充分測試;

團隊人數(shù)越來越多

這就要求開發(fā)人員了解與之業(yè)務(wù)相關(guān)的每一個業(yè)務(wù)模塊,防止出現(xiàn)某位開發(fā)人員修改代碼導致其他模塊出現(xiàn) bug 的情況,這個要求對于開發(fā)人員顯然是不友好的;

那怎樣解決模塊化開發(fā)的這些弊端呢?

當然是組件化嘍!

聊聊組件化

組件化可以說是 Android 中級開發(fā)工程師必備技能了,能有效解決許多單一項目下開發(fā)中出現(xiàn)的問題。

并且我要強調(diào)的是,組件化真的不難,還沒搞過的小伙伴不要慫。

什么是組件化

組件,顧名思義,“組裝的零件”,術(shù)語上叫做軟件單元,可用于組裝在應(yīng)用程序中。

所以,組件化,要更關(guān)注可復用性、更注重關(guān)注點分離、功能單一、高內(nèi)聚、粒度更小、是業(yè)務(wù)上能劃分的最小單元,畢竟是“組裝的零件”嘛!

從這個角度上看,組件化的粒度,似乎要比模塊化的粒度更小。

不過,我個人認為,要把組件化拆分到如此小的粒度,不可能,也沒有必要。在組件化項目的實際開發(fā)中,組件化的粒度,是要比模塊化的粒度更大的。

組件化的好處

首先要說的是,上述模塊化的好處,組件化都有,不再贅述;上述模塊化的弊端,組件化都給解決了,具體如下:

  1. 組件,既可以作為 library,又可以單獨作為 application,便于單獨編譯單獨測試,大大的提高了編譯和開發(fā)效率;

  2. (業(yè)務(wù))組件,可有自己獨立的版本,業(yè)務(wù)線互不干擾,可單獨編譯、測試、打包、部署;

  3. 各業(yè)務(wù)線共有的公共模塊可開發(fā)為組件,作為依賴庫供各業(yè)務(wù)線調(diào)用,減少重復代碼編寫,減少冗余,便于維護;

  4. 通過 gradle 配置文件,可對第三方庫進行統(tǒng)一管理,避免版本沖突,減少冗余;

  5. 通過 gradle 配置文件,可實現(xiàn) application 與 library 靈活組合與拆分,可以更快速的響應(yīng)需求方對功能模塊的選擇。

組件化實踐

首先要說明的是,下述是一個簡單的不能再簡單的組件化案例,只求幫助大家搭建起組件化的架構(gòu),功能上極其簡約。

九層之臺,起于累土。我們這就開始搭組件化的架構(gòu)吧!

組件化架構(gòu)

先上一張組件化項目整體架構(gòu)圖
image.png

其中的“業(yè)務(wù)組件”,既可以作為 application 單獨打包為 apk,又可以作為 library 靈活組合為綜合一些的應(yīng)用程序。

大多數(shù)開發(fā)者做組件化時面對的業(yè)務(wù)需求,都是上面這種情況。

我司的需求略有不同,不是將子業(yè)務(wù)組件組合為整體應(yīng)用程序,而是反其道而行之,需要將已上線項目拆分給不同的業(yè)務(wù)公司使用,在不同業(yè)務(wù)系統(tǒng)中,項目的邏輯和代碼會有區(qū)別,且版本不統(tǒng)一。

基于此,我搭建項目架構(gòu)如下圖所示,其中“m_moudle_main”是公司主要的、且邏輯和代碼相同的業(yè)務(wù)組件,“b_moudle_north”和“b_moudle_south”是拆分出來的業(yè)務(wù)組件,管理各自私有的邏輯和代碼,且版本有差別。
image.png

從Android工程看,結(jié)構(gòu)如下圖所示:

image.png

注:取moudle名,手動加上“b_” “m_” “x_”這樣的前綴,只是為了便于分辨組件層次。

統(tǒng)一配置文件

在項目根目錄下,自建 config.gradle 文件,對項目進行全局統(tǒng)一配置,并對版本和依賴進行統(tǒng)一管理,源碼如下:

/**
 * 全局統(tǒng)一配置
 */
ext {
    /**
     * module開關(guān)統(tǒng)一聲明在此處
     * true:module作為application,可單獨打包為apk
     * false:module作為library,可作為宿主application的組件
     */
    isNorthModule = false
    isSouthModule = false

    /**
     * 版本統(tǒng)一管理
     */
    versions = [
            applicationId           : "com.niujiaojian.amd",        //應(yīng)用ID
            versionCode             : 100,                    //版本號
            versionName             : "1.0.0",              //版本名稱

            compileSdkVersion       : 28,
            minSdkVersion           : 21,
            targetSdkVersion        : 28,

            androidSupportSdkVersion: "28.0.0",
            constraintlayoutVersion : "1.1.3",
            runnerVersion           : "1.1.0-alpha4",
            espressoVersion         : "3.1.0-alpha4",
            junitVersion            : "4.12",
            annotationsVersion      : "28.0.0",
            appcompatVersion        : "1.0.0-beta01",
            designVersion           : "1.0.0-beta01",

            multidexVersion         : "1.0.2",

            butterknifeVersion      : "10.1.0",

            arouterApiVersion       : "1.4.1",
            arouterCompilerVersion  : "1.2.2",
            arouterAnnotationVersion: "1.0.4"
    ]

    dependencies = [
            "appcompat"           : "androidx.appcompat:appcompat:${versions["appcompatVersion"]}",
            "constraintlayout"    : "androidx.constraintlayout:constraintlayout:${versions["constraintlayoutVersion"]}",
            "runner"              : "androidx.test:runner:${versions["runnerVersion"]}",
            "espresso_core"       : "androidx.test.espresso:espresso-core:${versions["espressoVersion"]}",
            "junit"               : "junit:junit:${versions["junitVersion"]}",
            //注釋處理器
            "support_annotations" : "com.android.support:support-annotations:${versions["annotationsVersion"]}",
            "design"              : "com.google.android.material:material:${versions["designVersion"]}",

            //方法數(shù)超過65535解決方法64K MultiDex分包方法
            "multidex"            : "androidx.multidex:multidex:2.0.0",

            //阿里路由
            "arouter_api"         : "com.alibaba:arouter-api:${versions["arouterApiVersion"]}",
            "arouter_compiler"    : "com.alibaba:arouter-compiler:${versions["arouterCompilerVersion"]}",
            "arouter_annotation"  : "com.alibaba:arouter-annotation:${versions["arouterAnnotationVersion"]}",

            //黃油刀
            "butterknife"         : "com.jakewharton:butterknife:${versions["butterknifeVersion"]}",
            "butterknife_compiler": "com.jakewharton:butterknife-compiler:${versions["butterknifeVersion"]}"
    ]
}
復制代碼

然后在project的build.gradle中引入config.gradle文件:

apply from: "config.gradle"
復制代碼

基礎(chǔ)公共組件

基礎(chǔ)公共組件 common 將一直作為 library 存在,所有業(yè)務(wù)組件都需要依賴 common 組件。

common 組件主要負責封裝公共部分,如網(wǎng)絡(luò)請求、數(shù)據(jù)存儲、自定義控件、各種工具類等,以及對第三方庫進行統(tǒng)一依賴等。

下圖是我的 common 組件的包結(jié)構(gòu)圖:

image.png

前文有言,common 組件還負責對第三方庫進行統(tǒng)一依賴,這樣上層業(yè)務(wù)組件就不需要再對第三方庫進行重復依賴了,其 build.gradle 源碼如下所示:

apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

……

dependencies {
    // 在項目中的libs中的所有的.jar結(jié)尾的文件,都是依賴
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    //把implementation 用api代替,它是對外部公開的, 所有其他的module就不需要添加該依賴
    api rootProject.ext.dependencies["appcompat"]
    api rootProject.ext.dependencies["constraintlayout"]
    api rootProject.ext.dependencies["junit"]
    api rootProject.ext.dependencies["runner"]
    api rootProject.ext.dependencies["espresso_core"]
    //注釋處理器,butterknife所必需
    api rootProject.ext.dependencies["support_annotations"]

    //MultiDex分包方法
    api rootProject.ext.dependencies["multidex"]

    //Material design
    api rootProject.ext.dependencies["design"]

    //黃油刀
    api rootProject.ext.dependencies["butterknife"]
    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]

    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
    api rootProject.ext.dependencies["arouter_api"]
    api rootProject.ext.dependencies["arouter_annotation"]

}
復制代碼

業(yè)務(wù)組件

業(yè)務(wù)組件在 library 模式下,向上組合為整體性項目;在 application 模式下,可獨立運行。

其 build.gradle 源碼如下:

if (Boolean.valueOf(rootProject.ext.isModule_North)) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'com.jakewharton.butterknife'

……

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

    //公用依賴庫
    implementation project(':x_module_common')
    implementation project(':m_module_main')
    //黃油刀
    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
}
復制代碼

至此,組件化架構(gòu)的搭建就算完成了。

可還有幾個問題,是組件化開發(fā)中必須要關(guān)注的,也是項目做組件化改造時可能會遭遇的難點,我們一起來看看吧。

組件化必須要關(guān)注的幾個問題

Application

在 common 組件中有 BaseAppliaction,提供全局唯一的 context,上層業(yè)務(wù)組件在組件化模式下,均需繼承于 BaseAppliaction。

/**
 * 基礎(chǔ) Application,所有需要模塊化開發(fā)的 module 都需要繼承自此 BaseApplication。
 */
public class BaseApplication extends Application {

    //全局唯一的context
    private static BaseApplication application;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        application = this;
        //MultiDexf分包初始化,必須最先初始化
        MultiDex.install(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        initARouter();
    }

    /**
     * 初始化路由
     */
    private void initARouter() {
        if (BuildConfig.DEBUG) {
            ARouter.openLog();  // 打印日志
            ARouter.openDebug(); // 開啟調(diào)試模式(如果在InstantRun模式下運行,必須開啟調(diào)試模式!線上版本需要關(guān)閉,否則有安全風險)
        }
        ARouter.init(application);// 盡可能早,推薦在Application中初始化
    }

    /**
     * 獲取全局唯一上下文
     *
     * @return BaseApplication
     */
    public static BaseApplication getApplication() {
        return application;
    }
復制代碼

applicationId 管理

可為不同組件設(shè)置不同的 applicationId,也可缺省,在Android Studio中,默認的 applicationId 與包名一致。

組件的 applicationId 在其 build.gradle 文件的 defaultConfig 中進行配置:

if (Boolean.valueOf(rootProject.ext.isModule_North)) {
    //組件模式下設(shè)置applicationId
    applicationId "com.niujiaojian.amd.north"
}
復制代碼

manifest.xml 管理

組件在 library 模式和 application 模式下,需要配置不同的 manifest.xml 文件,因為在 application 模式下,程序入口 Activity 和自定義的 Application 是不可或缺的。

在組件的 build.gradle文件 的 android 中進行 manifest 的管理:

/*
    * java插件引入了一個概念叫做SourceSets,通過修改SourceSets中的屬性,
    * 可以指定哪些源文件(或文件夾下的源文件)要被編譯,
    * 哪些源文件要被排除。
    * */
    sourceSets {
        main {
            if (Boolean.valueOf(rootProject.ext.isModule_North)) {//apk
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    //library模式下,排除java/debug文件夾下的所有文件
                    exclude '*module'
                }
            }
        }
    }
復制代碼

資源名沖突問題

資源名沖突問題,相信大家多多少少都遇到過,以前最常見的就是第三方 sdk 導致的資源名沖突了。

這個問題沒有特別好的解決辦法,只能通過設(shè)置資源名前綴 resourcePrefix 以及約束自己開發(fā)習慣進行解決。

資源名前綴 resourcePrefix ,是在 Project 的 build.gradle 中進行設(shè)置的:

/**
 * 限定所有子類xml中的資源文件的前綴
 * 注意:圖片資源,限定失效,需要手動添加前綴
 * */
subprojects {
    afterEvaluate {
        android {
            resourcePrefix "${project.name}_"
        }
    }
}
復制代碼

這樣設(shè)置完之后,string、style、color、dimens 等中資源名,必須以設(shè)置的字符串為前綴,而 layout、drawable 文件夾下的 shape 的 xml 文件的命名,必須以設(shè)置的字符串為前綴,否則會報錯提示。

另外,資源前綴的設(shè)置對圖片的命名無法限定,建議大家約束自己的開發(fā)習慣,自覺加上前綴。

建議:將 color、shape、style 這些放在基礎(chǔ)庫組件中去,這些資源不會太多,且復用性極高,所有業(yè)務(wù)組件又都會依賴基礎(chǔ)庫組件。

Butterknife R2 問題

Butterknife 存在的問題是控件 id 找不到,只要將 R 替換為 R2 即可解決問題。

需要注意的是,在如下代碼示例外的位置,不要這樣做,保持使用 R 即可,如 setContentView(R.layout.b_module_north_activity_splash)

public class SplashActivity extends BaseActivity {

    @BindView(R2.id.btn_toMain)
    Button btnToMain;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.b_module_north_activity_splash);
        ButterKnife.bind(this);
    }

    ……

    @OnClick(R2.id.btn_toMain)
    public void onViewClicked() {
    }
}
復制代碼

另外要注意的是,每一個使用 Butterknife 的組件,在其 build.gradle 的 dependencies 都要配置注解處理器處理其 compiler 庫:

apply plugin: 'com.jakewharton.butterknife'

……

dependencies {

    ……

    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
}
復制代碼

組件間跳轉(zhuǎn)

由于業(yè)務(wù)組件間不存在依賴關(guān)系,不可以通過 Intent 進行顯式跳轉(zhuǎn)。

若需跳轉(zhuǎn),是要借助于路由的,我使用的是阿里的開源框架 ARouter。

注:我在案例中只使用了 ARouter 的基礎(chǔ)的頁面跳轉(zhuǎn)功能,更復雜的諸如攜帶參數(shù)跳轉(zhuǎn)、聲明攔截器等功能的使用方法,大家可到 Github 上查看其使用文檔。

在每一個需要用到 ARouter 的組件的 build.gradle 文件中對其進行配置:

android {
   ...
       defaultConfig {
         ...
        //Arouter路由配置
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                includeCompileClasspath = true
            }
        }
    }
}
dependencies{
     ...
    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
}
復制代碼

跳轉(zhuǎn)目標頁面配置:

@Route(path = "/main/MainActivity")
public class MainActivity extends BaseActivity {
   ……
}
復制代碼

跳轉(zhuǎn)來源頁面的跳轉(zhuǎn)代碼:

...
   ARouter.getInstance()
          .build("/main/MainActivity")
          .navigation();
...
復制代碼

后記

組件化優(yōu)勢多多,用起來爽的不要不要的。

其中快感來的最快的,當屬大大提升了編譯速度了。

最后的話我整理了一套組件化學習筆記及視頻,有需要的同學可以在這里自取。


?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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