基于Arouter實(shí)現(xiàn)的組件化方案說(shuō)明:
基于Arouter實(shí)現(xiàn)的組件化方案說(shuō)明:
一個(gè)項(xiàng)目欺殿,隨著業(yè)務(wù)的發(fā)展,模塊會(huì)變的越來(lái)越多饶囚,代碼量也會(huì)變的異常龐大军援,進(jìn)而可能開(kāi)發(fā)的人會(huì)越來(lái)越多,這種情況下如果還是基于單一工程架構(gòu)屏箍,那就需要每一個(gè)開(kāi)發(fā)者都熟悉所有的代碼绘梦,而且代碼之間耦合嚴(yán)重,一個(gè)模塊穿插著大量其他業(yè)務(wù)模塊的邏輯赴魁,嚴(yán)重的話(huà)可能使項(xiàng)目處于牽一發(fā)而動(dòng)全身卸奉,不想輕易修改的局面;而且龐大的單一工程項(xiàng)目會(huì)導(dǎo)致編譯速度極慢颖御,開(kāi)發(fā)者長(zhǎng)時(shí)間等待編譯結(jié)果榄棵,非常不利于開(kāi)發(fā)工作。所以潘拱,就需要一個(gè)靈活的架構(gòu)來(lái)解決這些問(wèn)題疹鳄,組件化架構(gòu)思想應(yīng)運(yùn)而生。
整體結(jié)構(gòu)
- common:基礎(chǔ)組件部分芦岂,與業(yè)務(wù)無(wú)關(guān)尚辑,需要所有組件共同依賴(lài)的部分,如:網(wǎng)絡(luò)請(qǐng)求封裝盔腔、圖片加載封裝杠茬、ui相關(guān)基類(lèi)、工具集合等(當(dāng)然這些內(nèi)容可以依據(jù)分層原則放在不同的基礎(chǔ)module中)
- router-comp:路由驅(qū)動(dòng)組件弛随,承載整個(gè)項(xiàng)目的路由工作
- comp1:業(yè)務(wù)組件1瓢喉,如視頻組件,可獨(dú)立運(yùn)行
- comp2:業(yè)務(wù)組件2舀透,如新聞組件栓票,可獨(dú)立運(yùn)行
- comp3:業(yè)務(wù)組件3,如視頻組件愕够,可獨(dú)立運(yùn)行
- app:殼工程走贪,用于將各個(gè)組件組裝成一個(gè)完成app
組件化所面臨的問(wèn)題:
- 集成模式與組件模式轉(zhuǎn)換(熱插拔)
- 組件之間頁(yè)面跳轉(zhuǎn)(路由)
- 組件之間通信、調(diào)用彼此服務(wù)
- 打包混淆
組件化的實(shí)現(xiàn)
針對(duì)上面所說(shuō)的幾個(gè)問(wèn)題惑芭,下面我們逐個(gè)說(shuō)明它們的解決方案坠狡,當(dāng)解決完這些問(wèn)題,你會(huì)發(fā)現(xiàn)遂跟,你已經(jīng)搭建了一個(gè)基于組件化的項(xiàng)目逃沿。下圖是一個(gè)完整的組件化項(xiàng)目結(jié)構(gòu):common是基礎(chǔ)組件module婴渡,作為library存在,需要所有組件依賴(lài)凯亮;comp1边臼、comp2作為組件存在,可配置成library或可獨(dú)立運(yùn)行的module假消;app是個(gè)殼柠并,通過(guò)組裝組件實(shí)現(xiàn)其價(jià)值。
集成模式與組件模式轉(zhuǎn)換(熱插拔)
Android工程通過(guò)gradle構(gòu)建富拗,通過(guò)配置每個(gè)module的gradle臼予,來(lái)實(shí)現(xiàn)module的不同表現(xiàn)。Android Studio的module有兩種屬性媒峡,分別是:
- application屬性:可獨(dú)立運(yùn)行,也就是我們的app
- library屬性:不可獨(dú)立運(yùn)行葵擎,被app依賴(lài)的庫(kù)
module屬性通過(guò)其目錄下的gradle文件配置谅阿,當(dāng)module屬性為application時(shí),該module作為完整的app存在酬滤,可以獨(dú)自運(yùn)行签餐,方便編譯和調(diào)試;當(dāng)module屬性為library時(shí)盯串,該module作為一個(gè)依賴(lài)庫(kù)氯檐,被殼工程依賴(lài)并組裝成一個(gè)app。那么如何讓這兩種模式可以自動(dòng)轉(zhuǎn)換呢体捏?如果每次切換模式的時(shí)候冠摄,都手動(dòng)去修改每個(gè)組件的配置,組件少的情況下還可以接受几缭,組件多了會(huì)非常不方便河泳,下面就讓我們來(lái)聊聊如何實(shí)現(xiàn)兩種模式的自動(dòng)轉(zhuǎn)換。
- 首先年栓,聲明全局配置變量拆挥,來(lái)標(biāo)識(shí)module的屬性(app or library),如在工程目錄下的build.gradle文件中聲明布爾變量ext.isModule,true代表組件作為可獨(dú)立運(yùn)行的app某抓,false代表組件作為被依賴(lài)的library纸兔,如下所示
buildscript {
ext.kotlin_version = '1.3.21'
ext.isModule = true //true-每個(gè)組件都是單獨(dú)的module,可獨(dú)立運(yùn)行 false-組件作為library存在
repositories {
google()
jcenter()
}
}
- 配置組件的build.gradle文件
//1
if (rootProject.ext.isModule) {
//可獨(dú)立運(yùn)行的app
apply plugin: 'com.android.application'
} else{
//被依賴(lài)的library
apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
defaultConfig {
//applicationId "com.study.comp1" //2 如果沒(méi)有否副,默認(rèn)包名為applicationId
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
//3
sourceSets {
main {
if(rootProject.ext.isModule){
manifest.srcFile 'src/main/java/module/AndroidManifest.xml'
} else{
manifest.srcFile 'src/main/java/library/AndroidManifest.xml'
java {//移除module包下的代碼
exclude 'module'
}
}
}
}
}
上面是截取的組件gradle的部分代碼汉矿,包含了組件化需要配置的所有內(nèi)容,每一點(diǎn)都進(jìn)行了注釋
- 注釋1:如上所述备禀,根據(jù)isModule的值负甸,來(lái)設(shè)置module的屬性流强,作為app or library
- 注釋2:當(dāng)module屬性為library時(shí),不能設(shè)置applicationId呻待;當(dāng)為app時(shí)打月,如果未設(shè)置applicationId,默認(rèn)包名為applicationId蚕捉,所以為了方便奏篙,此處不設(shè)置applicationId
- 注釋3:Android Studio會(huì)為每個(gè)module生成對(duì)應(yīng)的AndroidManifest.xml文件,聲明自身需要的權(quán)限迫淹、四大組件秘通、數(shù)據(jù)等內(nèi)容;當(dāng)module屬性為app時(shí)敛熬,其對(duì)應(yīng)的AndroidManifest.xml需要具備完整app所需要的所有配置肺稀,尤其是聲明Application和launch的Activity;當(dāng)module屬性為library時(shí)应民,如果每個(gè)組件都聲明自己的Application和launch的Activity话原,那在合并的時(shí)候就會(huì)發(fā)生沖突,編譯也不會(huì)通過(guò)诲锹,所以繁仁,就需要為當(dāng)前module重新定義一個(gè)AndroidManifest.xml文件,不聲明Application和launch的Activity归园,然后根據(jù)isModule的值指定AndroidManifest.xml的路徑黄虱,因此就有了注釋3處的寫(xiě)法。為了避免集成模式下的命名沖突庸诱,每個(gè)文件都以自身module名為前綴來(lái)命名會(huì)是一個(gè)很好的方法捻浦。下圖是該module的目錄
- 在需要切換module屬性的時(shí)候改變步驟1處聲明的變量值,然后重新編譯即可
組件之間頁(yè)面跳轉(zhuǎn)(路由)
在組件化架構(gòu)中桥爽,不同的組件之間是平衡的默勾,不存在相互依賴(lài)的關(guān)系(可參考文章開(kāi)頭的架構(gòu)圖)。因此聚谁,假設(shè)在組件A中母剥,想要跳轉(zhuǎn)到組件B中的頁(yè)面,如果使用Intent顯式跳轉(zhuǎn)就行不通了形导,而且大家都知道环疼,Intent隱式跳轉(zhuǎn)管理起來(lái)非常不方便,所以Arouter出現(xiàn)了朵耕,并且有強(qiáng)大的技術(shù)團(tuán)隊(duì)支持炫隶,可以放心使用了。那么如何在組件化架構(gòu)中應(yīng)用Arouter呢阎曹?下面詳細(xì)來(lái)聊一聊
- 依賴(lài)處理
在common組件中將Arouter依賴(lài)進(jìn)來(lái)伪阶,并配置編譯參數(shù)煞檩;在業(yè)務(wù)組件中引入arouter編譯器插件,同時(shí)配置編譯器參數(shù)栅贴,下面是Common組件gradle文件的部分片段
//配置arouter編譯器參數(shù)斟湃,每個(gè)組件都需配置
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
dependencies {
//arouter api,只需在common組件中引入一次
api('com.alibaba:arouter-api:1.4.1') {
exclude group: 'com.android.support'
}
//arouter編譯器插件檐薯,每個(gè)組件都需引入
kapt 'com.alibaba:arouter-compiler:1.2.2'
}
- 初始化及編碼實(shí)現(xiàn)
在組件架構(gòu)中凝赛,經(jīng)常會(huì)遇到組件需要使用全局Context的情況,當(dāng)組件屬性為app時(shí)坛缕,可以通過(guò)自定義Application實(shí)現(xiàn)墓猎;當(dāng)組件屬性為library時(shí),由于組件被app依賴(lài)赚楚,導(dǎo)致無(wú)法調(diào)用app的Application實(shí)例毙沾,而且自身不存在Application;所以宠页,這里給出的方案是在common組件中創(chuàng)建一個(gè)BaseApplication左胞,然后讓集成模式(組件模式)下的Application繼承BaseApplication,在BaseApplication中獲取全局Context,并做一些初始化的工作勇皇,這里需要初始化Arouter罩句,如下是在common組件中聲明的BaseApplication
abstract class BaseApplication : Application() {
companion object {
var _context: Application? = null
//獲取全局Context
fun getContext(): Application {
return _context!!
}
}
override fun onCreate() {
super.onCreate()
_context = this
//初始化Arouter
initARouter()
//初始化其他第三方庫(kù)
}
private fun initARouter() {
if (BuildConfig.DEBUG) {
ARouter.openDebug()
ARouter.openLog()
}
ARouter.init(this)
}
override fun onTerminate() {
super.onTerminate()
//清理Arouter注冊(cè)表
ARouter.getInstance().destroy()
}
}
根據(jù)Arouter的路由特性焚刺,初始化之后敛摘,就可以通過(guò)@Route注解注冊(cè)頁(yè)面,然后調(diào)用Arouter api實(shí)現(xiàn)頁(yè)面的跳轉(zhuǎn)了(這里所謂的跨組件頁(yè)面跳轉(zhuǎn)是指在集成模式下乳愉,而非組件模式下)兄淫,無(wú)關(guān)乎是否在同一個(gè)組件下面,
假設(shè)我們要從組件1頁(yè)面攜帶參數(shù)跳轉(zhuǎn)到組件2頁(yè)面蔓姚,請(qǐng)看下面示例
/**
* 在組件2中通過(guò)@Route注解注冊(cè)該頁(yè)面
*/
@Route(path = "/comp2/msg",name = "我是組件2的MSGActivity")
class Comp2MsgActivity : BaseActivity() {
//傳遞過(guò)來(lái)的參數(shù)
@Autowired(name = "msg")
@JvmField
var msg: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//注入傳遞的參數(shù)
ARouter.getInstance().inject(this)
setContentView(R.layout.comp2_activity_msg)
comp2_msg_msg.text = msg!!
}
}
//在組件1中發(fā)起跳轉(zhuǎn)命令
ARouter.getInstance()
.build("/comp2/msg")
.withString("msg", "hello Im from Comp1")
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.navigation()
以上便完成了一次簡(jiǎn)單的跨越組件的頁(yè)面跳轉(zhuǎn)捕虽,僅僅是Arouter的基本使用而已。解決了組件間頁(yè)面跳轉(zhuǎn)的問(wèn)題后坡脐,我們來(lái)看看組件之間通信泄私、調(diào)用彼此服務(wù)的實(shí)現(xiàn)。
組件之間通信备闲、調(diào)用彼此服務(wù)
組件間通信功能和路由功能有著共通的地方晌端,即都是利用Arouter的基礎(chǔ)功能實(shí)現(xiàn),在Arouter驅(qū)動(dòng)層定義各個(gè)組件對(duì)外提供的接口恬砂,然后在組件自身模塊實(shí)現(xiàn)該接口咧纠,通過(guò)Arouter調(diào)用其他組件服務(wù)。假設(shè)我們?cè)诮M件2中需要調(diào)用組件1中的服務(wù)泻骤,可以總結(jié)為以下3點(diǎn)
- 定義接口:在common組件中定義組件1對(duì)外提供的接口CompServer1漆羔,注意該接口類(lèi)型為Arouter模板類(lèi)型IProvider
/**
* 組件1對(duì)外提供的接口
*/
interface CompServer1 : IProvider {
fun showMsg(msg: String)
}
- 實(shí)現(xiàn)接口:在comm1中實(shí)現(xiàn)上面定義的接口梧奢,并通過(guò)@Route注解注冊(cè)
@Route(path = "/comp1/server",name = "comp1對(duì)外提供的服務(wù)")
class CompServer : CompServer1 {
var mContext: Context? = null
override fun showMsg(msg: String) {
Toast.makeText(mContext,msg,Toast.LENGTH_SHORT).show()
}
override fun init(context: Context?) {
this.mContext = context!!
}
}
- 調(diào)用服務(wù):在完成組件1接口的定義和實(shí)現(xiàn)之后,在組件2中需要的地方調(diào)用該接口即可
val server1 = ARouter.getInstance().build("/comp1/server").navigation() as CompServer1
server1.showMsg("我從comp2吊起了comp1的接口")
有沒(méi)有感覺(jué)很簡(jiǎn)單演痒?亲轨?沒(méi)錯(cuò),就是這么簡(jiǎn)單嫡霞,趕緊去用吧!哈哈
打包混淆
說(shuō)到混淆瓶埋,有人可能會(huì)疑惑,如果在各個(gè)組件中混淆可不可以诊沪?不建議這樣混淆Q病!因?yàn)榻M件在集成模式下被gradle構(gòu)建成了release類(lèi)型的aar包端姚,如果在組件中進(jìn)行混淆晕粪,一旦代碼出現(xiàn)了bug,這個(gè)時(shí)候就很難根據(jù)日志去追蹤bug產(chǎn)生的原因渐裸,而且不同組件分別進(jìn)行混淆非常不方便維護(hù)和修改巫湘,這也是不推薦在業(yè)務(wù)組件中配置buildType(構(gòu)建類(lèi)型)的原因。
所以昏鹃,組件化項(xiàng)目的代碼混淆放在集成模式下的app殼工程尚氛,各個(gè)業(yè)務(wù)組件不配置混淆。集成模式下在app殼工程.gradle文件的release構(gòu)建模式下開(kāi)啟混淆洞渤,其他buildType配置和普通項(xiàng)目相同阅嘶,混淆文件放在app殼工程下,各個(gè)組件的代碼混淆均放在該混淆文件中载迄。
小結(jié)
以上讯柔,我們已經(jīng)逐一解決了組件化所面對(duì)的各個(gè)問(wèn)題,至此护昧,我們已經(jīng)搭建了一個(gè)簡(jiǎn)單的組件化架構(gòu)的項(xiàng)目魂迄,這一起感覺(jué)是在不知不覺(jué)中就實(shí)現(xiàn)了,并不是很難哦惋耙!現(xiàn)在捣炬,我們總結(jié)一下組件化的優(yōu)勢(shì)了
- 解耦:將業(yè)務(wù)組件代碼90%與工程解耦,只所以是90%而非100%绽榛,是因?yàn)闃I(yè)務(wù)組件需要在common組件中聲明對(duì)外開(kāi)放的接口湿酸,那有沒(méi)有什么方式可以做到完全解耦呢?目前還沒(méi)有發(fā)現(xiàn)更好的辦法蒜田。稿械。。
- 提高開(kāi)發(fā)效率:依賴(lài)解耦這一優(yōu)勢(shì)冲粤,團(tuán)隊(duì)成員可以只專(zhuān)注于自己負(fù)責(zé)的組件美莫,開(kāi)發(fā)效率更高页眯;而且,組件開(kāi)發(fā)過(guò)程中只需編譯自身的module厢呵,這樣大大縮短了編譯時(shí)長(zhǎng)窝撵,避免了漫長(zhǎng)的等待編譯局面。
- 結(jié)構(gòu)清晰:在業(yè)務(wù)組件明確拆分的前提下襟铭,項(xiàng)目結(jié)構(gòu)變的異常清晰碌奉,非常方便全局掌控。