前言
組件化是什么,是把一個功能完整的 App 或模塊拆分成多個子模塊, 表現(xiàn)在androidStudio項目工程里就是分多個module。每個子模塊可以獨(dú)立編譯和運(yùn)行, 模塊之間可以任意組合成另一個新的 App 或模塊, 每個模塊不必須相互依賴但可以相互調(diào)起和通信。
組件化的意義,對于一個小型項目來說可能覺得多此一舉,但是對于一個中型以上的項目,組件化還是非常有意義的。APP版本不斷的迭代,新功能的不斷增加,業(yè)務(wù)也會變的越來越復(fù)雜,的代碼也變的越來越多,代碼耦合嚴(yán)重,影響開發(fā)效率,增加項目的維護(hù)成本,編譯代碼的時間長,每修改一處代碼后都要重新編譯整個項目。組件化的出現(xiàn)就是解決以上問題,并且還具備自由組裝成新的模塊的優(yōu)點(diǎn)。
我研究組件化主要還是為了技術(shù)儲備和支持新業(yè)務(wù)的開發(fā),對于主項目要使用的話,就相當(dāng)于項目重構(gòu),比較費(fèi)時費(fèi)力,在各組都有KPI的情況下是不會給我們時間重構(gòu)的,閑話不多說,下面介紹組件化框架和搭建過程中注意的問題。
組件化架構(gòu)
直接上圖:
架構(gòu)介紹:
如圖一共分了4層:應(yīng)用層、業(yè)務(wù)組件、功能組件、基礎(chǔ)組件。
- 應(yīng)用層:我們的app殼工程,負(fù)責(zé)管理各個業(yè)務(wù)組件,和打包apk,沒有具體的業(yè)務(wù)功能。
- 業(yè)務(wù)組件:具體業(yè)務(wù)而獨(dú)立形成一個的工程,可以單獨(dú)運(yùn)行提供功能。
- 功能組件:APP的某些基礎(chǔ)功能,可單獨(dú)編譯,但不會單獨(dú)發(fā)布提供功能apk。
- 基礎(chǔ)組件:開源的第三方的庫。
Eventbus 是用來各層之間的通信,組件路由是用來實(shí)現(xiàn)組件之間的通信和調(diào)起。
網(wǎng)上有的文章有不同的分層發(fā),有人分三層把業(yè)務(wù)組件和功能組件統(tǒng)稱組件層。有人對業(yè)務(wù)組件進(jìn)行細(xì)分,如Main組件。整體是一樣的,關(guān)鍵大家能理解整個架構(gòu)。
組件化項目結(jié)構(gòu)介紹
直接上圖:
大家看到有多個module,以app_開頭的是可以單獨(dú)打包發(fā)布的module,也就是app殼module,和業(yè)務(wù)組件module,module開頭的是不單獨(dú)打包發(fā)布的組件,就是功能組件。我們一一說明:
- app:app殼,就是我們主項目。
- app_radio:電臺業(yè)務(wù)模塊,可以單獨(dú)作為電臺app發(fā)布,也是app主項目的一個功能模塊。
- module_core:項目的基礎(chǔ)核心組件,說是基礎(chǔ),他是對基礎(chǔ)組件的封裝,包括架構(gòu)圖中的Glide、Retrofit、Rxjava等的封裝,提供基礎(chǔ)功能。說是核心,他是對項目基礎(chǔ)架構(gòu)的封裝,這里使用的是MVP架構(gòu)。
- module_commons:封裝組件共用的類和資源,各組件模塊解耦之后,避免不了一些共用的類和資源,比如實(shí)體類、錯誤頁、dialog等。
- module_router:封裝組件路由,實(shí)現(xiàn)組件調(diào)起和通信。
- module_share / module_playserservice:我們項目用到的功能組件,不是必要的。
網(wǎng)上有同學(xué)有不同的module分發(fā),比如module_commons進(jìn)一步細(xì)分公共的類module和資源module。大家可以根據(jù)實(shí)際情況細(xì)分。到這里大家會發(fā)現(xiàn)組件化的一個弊端,就是項目會有很多的module。
組件化搭建注意問題
組建如何單獨(dú)編譯
思路就是在build.gradle的區(qū)分是apply plugin: 'com.android.application'還是apply plugin: 'com.android.library'。具體實(shí)現(xiàn):
在gradle.properties文件添加一個判斷屬性
isBuildAsModule=false
在build.gradle里根據(jù)屬性加載不同插件:
if(isBuildAsModule.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
在兩種情況下AndroidManifest.xml文件是有差別的。作為獨(dú)立運(yùn)行的app,有自己的Application,要加Launcher的入口intent,而作為library不需要。所以需要寫兩個不同的AndroidManifest.xml即可,通過isBuildAsModule加以區(qū)分。
if (isBuildAsModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/library/AndroidManifest.xml'
}
這種方式同時避免了AndroidManifest.xml清單文件沖突問題。
其他需要區(qū)分兩種編譯模式,原理相同。
基礎(chǔ)庫和SDK版本號統(tǒng)一
這是多module開發(fā)需要解決的問題,不同module的依賴sdk版本不一致或者引入的庫版本不一致會導(dǎo)致編譯問題和兼容性問題。解決辦法是在主項目最外層用一個config.xml文件來統(tǒng)一管理基礎(chǔ)庫和sdk的版本。config.xml部分代碼如下:
def retrofit_version = '2.3.0'
def okhttp_version = '3.9.0'
def dagger_version = '2.11'
def autodispose_version = "0.5.1"
def support_version = '27.1.0'
def espresso_version = '2.2.2'
def glide_version = '4.6.1'
def butterknife_version = '8.8.1'
def routerVersion = "1.2.4"
def routerCompilerVersion = "1.1.4"
project.ext {
android = [
compileSdkVersion: 27,
buildToolsVersion: "27.0.3",
applicationId : "com.taihe.music.mvpsample",
minSdkVersion : 16,
targetSdkVersion : 27,
versionCode : 1,
versionName : "1.0"
]
dependencies = [
//android-support
"support-v4" : "com.android.support:support-v4:${support_version}",
"appcompat-v7" : "com.android.support:appcompat-v7:${support_version}",
..........
使用config.xml,時在最外層build.gradle配置config文件:
apply from: "config.gradle"
具體引入基礎(chǔ)庫的地方在各module的build.gradle文件里:
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
.....
//glide
api rootProject.ext.dependencies["glide"]
annotationProcessor rootProject.ext.dependencies["glide-compiler"]
組件之間資源名沖突
組件之間如果資源命名相同,就會產(chǎn)生沖突,解決這個問題最簡單的辦法就是在項目中制定資源文件命名規(guī)范,比如app_radio組件所有資源以radio_開頭,所有開發(fā)人員必須遵守規(guī)范。
當(dāng)然,gladle也給我提供了解決方案,就是在build.gradle中添加如下的代碼:
resourcePrefix "radio_"
設(shè)置了這個屬性后,所有的資源名必須以指定的字符串做前綴,否則會報錯。而且這種形式只能限定xml里面的資源,并不能限定圖片資源,我們?nèi)匀恍枰謩尤バ薷馁Y源名;所以不推薦使用這種方法來解決資源名沖突。
基礎(chǔ)組件依賴問題
所有的基礎(chǔ)組件我們封裝在module_core中,這樣就存在一個問題,我們項目依賴了module_core就默認(rèn)依賴了所有的基礎(chǔ)組件,但實(shí)際情況中我們是不需要依賴所有的,或者module_core中的基礎(chǔ)組件和其他模塊有沖突的情況。所以我們需要排除掉用不到和重復(fù)的庫,Gradle支持兩種排除方式,根據(jù)組件名排除或者根據(jù)包名排除,代碼如下:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar']) compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
exclude module: 'support-v4'//根據(jù)組件名排除
exclude group: 'android.support.v4'//根據(jù)包名排除
}
}
組件之間跳轉(zhuǎn)和通信
組件之間的頁面調(diào)起,雖然我們可以使用intentFilter來實(shí)現(xiàn)Activity的隱式啟動,但是邏輯上存在耦合,并且不支持Fragment,所以這里我們使用的是阿里的組件路由Arouter。它可以實(shí)現(xiàn)Activity和Fragment的調(diào)起,支持依賴注入和數(shù)據(jù)通信。這里我們只介紹Activty調(diào)起,詳細(xì)使用可參考https://blog.csdn.net/zhaoyanjun6/article/details/76165252
我們以調(diào)起app_radio組件中的HomeActivity為例,HomeActivity類配置如下:
@Route(path = RouterConstants.RADIO_HOME_ACTIVITY)
public class HomeActivity extends BaseActivity<HomePresenter> implements HomeContract.View {
@BindView(R2.id.btnUserInfo)
Button button;
@BindView(R2.id.ivTest)
ImageView imageView;
........
調(diào)起HomeActivity的代碼:
ARouter.getInstance()
.build(RouterConstants.RADIO_HOME_ACTIVITY)
.withObject("user", userModel)
.navigation();
可以看出組件路由需要定義常量RouterConstants.RADIO_HOME_ACTIVITY來關(guān)聯(lián)調(diào)起頁面,withObject("user", userModel)是頁面間傳遞的數(shù)據(jù)。
手動單獨(dú)編譯
組件化的一個優(yōu)點(diǎn)就是可以單獨(dú)編譯,但是在開發(fā)過程發(fā)現(xiàn)有的時候修改了模塊module里一些配置文體之后,編譯主app,模塊module不會重新編譯,導(dǎo)致修改無效。所以在修改了配置文件之后最好手動編譯module確保修改有效。單獨(dú)編譯的入口在下圖:
動態(tài)變化網(wǎng)絡(luò)baseUrl
module_core中我使用Retrofit+OKhttp封裝的網(wǎng)絡(luò)層,并對外提供配置接口,包括配置baseUrl,主module中Applicatuion中會完成這些配置。問題來了,如果其他模塊使用baseUrl不同怎么處理。思路就是,對OkHttp進(jìn)行修改,無非就是使用攔截器,幸好好心人提供了開源庫RetrofitUrlManager可以解決該問題。詳細(xì)參考,https://github.com/JessYanCoding/RetrofitUrlManager
動態(tài)修改baseurl的代碼:
將OkHttpClient.Builder傳給RetrofitUrlManager。
RetrofitUrlManager.getInstance().with(builder);
以app_radio配置baseUrl為例:
RetrofitUrlManager.getInstance().putDomain(RADIO_DOMAIN_NAME, RADIO_BASE_API);
RADIO_BASE_API為具體的baseUrl,RADIO_DOMAIN_NAME為對應(yīng)的key。
具體使用Retrofit來定義請求接口時:
@Headers({DOMAIN_NAME_HEADER + RADIO_DOMAIN_NAME})
@GET("users/{user}")
Maybe<UserInfo> getUserInfo(@Path("user") String user);
這樣就可以實(shí)現(xiàn)baseUrl的動態(tài)變化。同理如果其他的網(wǎng)絡(luò)配置不同也是使用攔截器是形式來修改,大家可以參考RetrofitUrlManager的實(shí)現(xiàn)。
本文從整體上介紹了組件化的搭建問題,沒有具體到基礎(chǔ)庫和框架的封裝,也就是module_core的搭建和使用。想要熟悉整體框架或者使用,首先要熟悉第三方庫的使用和原理,封裝的過程就是改造的過程所以要了解基本原理;其次再熟悉module_core的封裝代和使用,最后才是本文提到的組件化過程中的問題解決。
跟本文一樣,大部分開源組件化項目,基礎(chǔ)庫用到了Dagger、Rxjava。這兩個開源項目需要一定的學(xué)習(xí)成本,建議大家擇情而定是否使用。