前言
組件化技術,在 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ā)中凤瘦,組件化的粒度,是要比模塊化的粒度更大的案铺。
組件化的好處
首先要說的是蔬芥,上述模塊化的好處,組件化都有控汉,不再贅述笔诵;上述模塊化的弊端,組件化都給解決了暇番,具體如下:
組件嗤放,既可以作為 library思喊,又可以單獨作為 application壁酬,便于單獨編譯單獨測試,大大的提高了編譯和開發(fā)效率恨课;
(業(yè)務)組件舆乔,可有自己獨立的版本,業(yè)務線互不干擾剂公,可單獨編譯希俩、測試、打包纲辽、部署颜武;
各業(yè)務線共有的公共模塊可開發(fā)為組件,作為依賴庫供各業(yè)務線調(diào)用拖吼,減少重復代碼編寫鳞上,減少冗余,便于維護吊档;
通過 gradle 配置文件篙议,可對第三方庫進行統(tǒng)一管理,避免版本沖突,減少冗余鬼贱;
通過 gradle 配置文件移怯,可實現(xiàn) application 與 library 靈活組合與拆分,可以更快速的響應需求方對功能模塊的選擇这难。
組件化實踐
首先要說明的是,下述是一個簡單的不能再簡單的組件化案例雁佳,只求幫助大家搭建起組件化的架構(gòu)脐帝,功能上極其簡約。
九層之臺糖权,起于累土。我們這就開始搭組件化的架構(gòu)吧疚顷!
組件化架構(gòu)
先上一張組件化項目整體架構(gòu)圖其中的“業(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è)務組件,管理各自私有的邏輯和代碼网沾,且版本有差別癞蚕。從Android工程看,結(jié)構(gòu)如下圖所示:
注:取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)圖:
前文有言摄狱,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)勢多多,用起來爽的不要不要的薛耻。
其中快感來的最快的营罢,當屬大大提升了編譯速度了。
最后的話我整理了一套組件化學習筆記及視頻饼齿,有需要的同學可以在這里自取饲漾。