前言
組件化技術(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ā)中,組件化的粒度,是要比模塊化的粒度更大的。
組件化的好處
首先要說的是,上述模塊化的好處,組件化都有,不再贅述;上述模塊化的弊端,組件化都給解決了,具體如下:
組件,既可以作為 library,又可以單獨作為 application,便于單獨編譯單獨測試,大大的提高了編譯和開發(fā)效率;
(業(yè)務(wù))組件,可有自己獨立的版本,業(yè)務(wù)線互不干擾,可單獨編譯、測試、打包、部署;
各業(yè)務(wù)線共有的公共模塊可開發(fā)為組件,作為依賴庫供各業(yè)務(wù)線調(diào)用,減少重復代碼編寫,減少冗余,便于維護;
通過 gradle 配置文件,可對第三方庫進行統(tǒng)一管理,避免版本沖突,減少冗余;
通過 gradle 配置文件,可實現(xiàn) application 與 library 靈活組合與拆分,可以更快速的響應(yīng)需求方對功能模塊的選擇。
組件化實踐
首先要說明的是,下述是一個簡單的不能再簡單的組件化案例,只求幫助大家搭建起組件化的架構(gòu),功能上極其簡約。
九層之臺,起于累土。我們這就開始搭組件化的架構(gòu)吧!
組件化架構(gòu)
先上一張組件化項目整體架構(gòu)圖其中的“業(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ù)組件,管理各自私有的邏輯和代碼,且版本有差別。從Android工程看,結(jié)構(gòu)如下圖所示:
注:取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)圖:
前文有言,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)勢多多,用起來爽的不要不要的。
其中快感來的最快的,當屬大大提升了編譯速度了。
最后的話我整理了一套組件化學習筆記及視頻,有需要的同學可以在這里自取。

