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

前言

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

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

談談模塊化

要聊組件化瘸爽,慣例是要談談模塊化的,畢竟它與組件化確實有一些相同點乏冀,在組件化的項目中它也會與組件化發(fā)生關聯(lián)。

什么是模塊化

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

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

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

模塊化的好處

模塊化有哪些好處呢?

復用

首先多糠,基礎模塊累舷,可為業(yè)務模塊所復用;

其次夹孔,子業(yè)務模塊被盈,可為父業(yè)務模塊,甚至不同的項目所復用搭伤。

解耦

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

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

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

模塊化的弊端

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

有。

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

項目代碼量越來越大

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

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

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

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

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

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

當然是組件化嘍嘴瓤!

聊聊組件化

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

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

什么是組件化

組件狞贱,顧名思義,“組裝的零件”蜀涨,術語上叫做軟件單元瞎嬉,可用于組裝在應用程序中。

所以厚柳,組件化氧枣,要更關注可復用性、更注重關注點分離别垮、功能單一便监、高內(nèi)聚、粒度更小、是業(yè)務上能劃分的最小單元烧董,畢竟是“組裝的零件”嘛毁靶!

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

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

組件化的好處

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

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

  2. (業(yè)務)組件舆乔,可有自己獨立的版本,業(yè)務線互不干擾剂公,可單獨編譯希俩、測試、打包纲辽、部署颜武;

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

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

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

組件化實踐

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

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

組件化架構(gòu)

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

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

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

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

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

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

image.png

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

統(tǒng)一配置文件

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

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

    /**
     * 版本統(tǒng)一管理
     */
    versions = [
            applicationId           : "com.niujiaojian.amd",        //應用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"
復制代碼

基礎公共組件

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

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

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

image.png

前文有言摄狱,common 組件還負責對第三方庫進行統(tǒng)一依賴,這樣上層業(yè)務組件就不需要再對第三方庫進行重復依賴了无午,其 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è)務組件

業(yè)務組件在 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ā)中必須要關注的牡借,也是項目做組件化改造時可能會遭遇的難點拳昌,我們一起來看看吧袭异。

組件化必須要關注的幾個問題

Application

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

/**
 * 基礎 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)試模式睡互!線上版本需要關閉,否則有安全風險)
        }
        ARouter.init(application);// 盡可能早,推薦在Application中初始化
    }

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

applicationId 管理

可為不同組件設置不同的 applicationId,也可缺省就珠,在Android Studio中寇壳,默認的 applicationId 與包名一致。

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

if (Boolean.valueOf(rootProject.ext.isModule_North)) {
    //組件模式下設置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 導致的資源名沖突了。

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

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

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

這樣設置完之后,string增拥、style啄巧、color、dimens 等中資源名掌栅,必須以設置的字符串為前綴秩仆,而 layout、drawable 文件夾下的 shape 的 xml 文件的命名猾封,必須以設置的字符串為前綴澄耍,否則會報錯提示。

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

建議:將 color选酗、shape、style 這些放在基礎庫組件中去岳枷,這些資源不會太多芒填,且復用性極高呜叫,所有業(yè)務組件又都會依賴基礎庫組件。

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è)務組件間不存在依賴關系,不可以通過 Intent 進行顯式跳轉(zhuǎn)果覆。

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

注:我在案例中只使用了 ARouter 的基礎的頁面跳轉(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)勢多多,用起來爽的不要不要的薛耻。

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

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


?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缕溉,隨后出現(xiàn)的幾起案子考传,更是在濱河造成了極大的恐慌,老刑警劉巖证鸥,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件僚楞,死亡現(xiàn)場離奇詭異,居然都是意外死亡敌土,警方通過查閱死者的電腦和手機镜硕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門运翼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來返干,“玉大人,你說我怎么就攤上這事血淌【厍罚” “怎么了财剖?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長癌淮。 經(jīng)常有香客問我躺坟,道長尤筐,這世上最難降的妖魔是什么拯坟? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮果善,結(jié)果婚禮上虚倒,老公的妹妹穿的比我還像新娘美侦。我一直安慰自己,他們只是感情好魂奥,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布菠剩。 她就那樣靜靜地躺著,像睡著了一般耻煤。 火紅的嫁衣襯著肌膚如雪具壮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天哈蝇,我揣著相機與錄音棺妓,去河邊找鬼。 笑死炮赦,一個胖子當著我的面吹牛涧郊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播眼五,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼妆艘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了看幼?” 一聲冷哼從身側(cè)響起批旺,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诵姜,沒想到半個月后汽煮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡棚唆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年暇赤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宵凌。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡鞋囊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瞎惫,到底是詐尸還是另有隱情溜腐,我是刑警寧澤译株,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站挺益,受9級特大地震影響歉糜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜望众,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一匪补、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烂翰,春花似錦叉袍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至棵里,卻和暖如春润文,著一層夾襖步出監(jiān)牢的瞬間殿怜,已是汗流浹背典蝌。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工头谜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柱告。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓截驮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親葵袭。 傳聞我的和親對象是個殘疾皇子乖菱,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容