
基于Arouter實現(xiàn)的組件化方案說明:
基于Arouter實現(xiàn)的組件化方案說明:
一個項目,隨著業(yè)務(wù)的發(fā)展,模塊會變的越來越多,代碼量也會變的異常龐大,進而可能開發(fā)的人會越來越多,這種情況下如果還是基于單一工程架構(gòu),那就需要每一個開發(fā)者都熟悉所有的代碼,而且代碼之間耦合嚴重,一個模塊穿插著大量其他業(yè)務(wù)模塊的邏輯,嚴重的話可能使項目處于牽一發(fā)而動全身,不想輕易修改的局面;而且龐大的單一工程項目會導(dǎo)致編譯速度極慢,開發(fā)者長時間等待編譯結(jié)果,非常不利于開發(fā)工作。所以,就需要一個靈活的架構(gòu)來解決這些問題,組件化架構(gòu)思想應(yīng)運而生。
整體結(jié)構(gòu)
- common:基礎(chǔ)組件部分,與業(yè)務(wù)無關(guān),需要所有組件共同依賴的部分,如:網(wǎng)絡(luò)請求封裝、圖片加載封裝、ui相關(guān)基類、工具集合等(當然這些內(nèi)容可以依據(jù)分層原則放在不同的基礎(chǔ)module中)
- router-comp:路由驅(qū)動組件,承載整個項目的路由工作
- comp1:業(yè)務(wù)組件1,如視頻組件,可獨立運行
- comp2:業(yè)務(wù)組件2,如新聞組件,可獨立運行
- comp3:業(yè)務(wù)組件3,如視頻組件,可獨立運行
- app:殼工程,用于將各個組件組裝成一個完成app
組件化所面臨的問題:
- 集成模式與組件模式轉(zhuǎn)換(熱插拔)
- 組件之間頁面跳轉(zhuǎn)(路由)
- 組件之間通信、調(diào)用彼此服務(wù)
- 打包混淆
組件化的實現(xiàn)
針對上面所說的幾個問題,下面我們逐個說明它們的解決方案,當解決完這些問題,你會發(fā)現(xiàn),你已經(jīng)搭建了一個基于組件化的項目。下圖是一個完整的組件化項目結(jié)構(gòu):common是基礎(chǔ)組件module,作為library存在,需要所有組件依賴;comp1、comp2作為組件存在,可配置成library或可獨立運行的module;app是個殼,通過組裝組件實現(xiàn)其價值。
集成模式與組件模式轉(zhuǎn)換(熱插拔)
Android工程通過gradle構(gòu)建,通過配置每個module的gradle,來實現(xiàn)module的不同表現(xiàn)。Android Studio的module有兩種屬性,分別是:
- application屬性:可獨立運行,也就是我們的app
- library屬性:不可獨立運行,被app依賴的庫
module屬性通過其目錄下的gradle文件配置,當module屬性為application時,該module作為完整的app存在,可以獨自運行,方便編譯和調(diào)試;當module屬性為library時,該module作為一個依賴庫,被殼工程依賴并組裝成一個app。那么如何讓這兩種模式可以自動轉(zhuǎn)換呢?如果每次切換模式的時候,都手動去修改每個組件的配置,組件少的情況下還可以接受,組件多了會非常不方便,下面就讓我們來聊聊如何實現(xiàn)兩種模式的自動轉(zhuǎn)換。
- 首先,聲明全局配置變量,來標識module的屬性(app or library),如在工程目錄下的build.gradle文件中聲明布爾變量ext.isModule,true代表組件作為可獨立運行的app,false代表組件作為被依賴的library,如下所示
buildscript {
ext.kotlin_version = '1.3.21'
ext.isModule = true //true-每個組件都是單獨的module,可獨立運行 false-組件作為library存在
repositories {
google()
jcenter()
}
}
- 配置組件的build.gradle文件
//1
if (rootProject.ext.isModule) {
//可獨立運行的app
apply plugin: 'com.android.application'
} else{
//被依賴的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 如果沒有,默認包名為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)容,每一點都進行了注釋
- 注釋1:如上所述,根據(jù)isModule的值,來設(shè)置module的屬性,作為app or library
- 注釋2:當module屬性為library時,不能設(shè)置applicationId;當為app時,如果未設(shè)置applicationId,默認包名為applicationId,所以為了方便,此處不設(shè)置applicationId
- 注釋3:Android Studio會為每個module生成對應(yīng)的AndroidManifest.xml文件,聲明自身需要的權(quán)限、四大組件、數(shù)據(jù)等內(nèi)容;當module屬性為app時,其對應(yīng)的AndroidManifest.xml需要具備完整app所需要的所有配置,尤其是聲明Application和launch的Activity;當module屬性為library時,如果每個組件都聲明自己的Application和launch的Activity,那在合并的時候就會發(fā)生沖突,編譯也不會通過,所以,就需要為當前module重新定義一個AndroidManifest.xml文件,不聲明Application和launch的Activity,然后根據(jù)isModule的值指定AndroidManifest.xml的路徑,因此就有了注釋3處的寫法。為了避免集成模式下的命名沖突,每個文件都以自身module名為前綴來命名會是一個很好的方法。下圖是該module的目錄
- 在需要切換module屬性的時候改變步驟1處聲明的變量值,然后重新編譯即可
組件之間頁面跳轉(zhuǎn)(路由)
在組件化架構(gòu)中,不同的組件之間是平衡的,不存在相互依賴的關(guān)系(可參考文章開頭的架構(gòu)圖)。因此,假設(shè)在組件A中,想要跳轉(zhuǎn)到組件B中的頁面,如果使用Intent顯式跳轉(zhuǎn)就行不通了,而且大家都知道,Intent隱式跳轉(zhuǎn)管理起來非常不方便,所以Arouter出現(xiàn)了,并且有強大的技術(shù)團隊支持,可以放心使用了。那么如何在組件化架構(gòu)中應(yīng)用Arouter呢?下面詳細來聊一聊
- 依賴處理
在common組件中將Arouter依賴進來,并配置編譯參數(shù);在業(yè)務(wù)組件中引入arouter編譯器插件,同時配置編譯器參數(shù),下面是Common組件gradle文件的部分片段
//配置arouter編譯器參數(shù),每個組件都需配置
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編譯器插件,每個組件都需引入
kapt 'com.alibaba:arouter-compiler:1.2.2'
}
- 初始化及編碼實現(xiàn)
在組件架構(gòu)中,經(jīng)常會遇到組件需要使用全局Context的情況,當組件屬性為app時,可以通過自定義Application實現(xiàn);當組件屬性為library時,由于組件被app依賴,導(dǎo)致無法調(diào)用app的Application實例,而且自身不存在Application;所以,這里給出的方案是在common組件中創(chuàng)建一個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()
//初始化其他第三方庫
}
private fun initARouter() {
if (BuildConfig.DEBUG) {
ARouter.openDebug()
ARouter.openLog()
}
ARouter.init(this)
}
override fun onTerminate() {
super.onTerminate()
//清理Arouter注冊表
ARouter.getInstance().destroy()
}
}
根據(jù)Arouter的路由特性,初始化之后,就可以通過@Route注解注冊頁面,然后調(diào)用Arouter api實現(xiàn)頁面的跳轉(zhuǎn)了(這里所謂的跨組件頁面跳轉(zhuǎn)是指在集成模式下,而非組件模式下),無關(guān)乎是否在同一個組件下面,
假設(shè)我們要從組件1頁面攜帶參數(shù)跳轉(zhuǎn)到組件2頁面,請看下面示例
/**
* 在組件2中通過@Route注解注冊該頁面
*/
@Route(path = "/comp2/msg",name = "我是組件2的MSGActivity")
class Comp2MsgActivity : BaseActivity() {
//傳遞過來的參數(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()
以上便完成了一次簡單的跨越組件的頁面跳轉(zhuǎn),僅僅是Arouter的基本使用而已。解決了組件間頁面跳轉(zhuǎn)的問題后,我們來看看組件之間通信、調(diào)用彼此服務(wù)的實現(xiàn)。
組件之間通信、調(diào)用彼此服務(wù)
組件間通信功能和路由功能有著共通的地方,即都是利用Arouter的基礎(chǔ)功能實現(xiàn),在Arouter驅(qū)動層定義各個組件對外提供的接口,然后在組件自身模塊實現(xiàn)該接口,通過Arouter調(diào)用其他組件服務(wù)。假設(shè)我們在組件2中需要調(diào)用組件1中的服務(wù),可以總結(jié)為以下3點
- 定義接口:在common組件中定義組件1對外提供的接口CompServer1,注意該接口類型為Arouter模板類型IProvider
/**
* 組件1對外提供的接口
*/
interface CompServer1 : IProvider {
fun showMsg(msg: String)
}
- 實現(xiàn)接口:在comm1中實現(xiàn)上面定義的接口,并通過@Route注解注冊
@Route(path = "/comp1/server",name = "comp1對外提供的服務(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接口的定義和實現(xiàn)之后,在組件2中需要的地方調(diào)用該接口即可
val server1 = ARouter.getInstance().build("/comp1/server").navigation() as CompServer1
server1.showMsg("我從comp2吊起了comp1的接口")
有沒有感覺很簡單??沒錯,就是這么簡單,趕緊去用吧!哈哈
打包混淆
說到混淆,有人可能會疑惑,如果在各個組件中混淆可不可以?不建議這樣混淆!!因為組件在集成模式下被gradle構(gòu)建成了release類型的aar包,如果在組件中進行混淆,一旦代碼出現(xiàn)了bug,這個時候就很難根據(jù)日志去追蹤bug產(chǎn)生的原因,而且不同組件分別進行混淆非常不方便維護和修改,這也是不推薦在業(yè)務(wù)組件中配置buildType(構(gòu)建類型)的原因。
所以,組件化項目的代碼混淆放在集成模式下的app殼工程,各個業(yè)務(wù)組件不配置混淆。集成模式下在app殼工程.gradle文件的release構(gòu)建模式下開啟混淆,其他buildType配置和普通項目相同,混淆文件放在app殼工程下,各個組件的代碼混淆均放在該混淆文件中。
小結(jié)
以上,我們已經(jīng)逐一解決了組件化所面對的各個問題,至此,我們已經(jīng)搭建了一個簡單的組件化架構(gòu)的項目,這一起感覺是在不知不覺中就實現(xiàn)了,并不是很難哦!現(xiàn)在,我們總結(jié)一下組件化的優(yōu)勢了
- 解耦:將業(yè)務(wù)組件代碼90%與工程解耦,只所以是90%而非100%,是因為業(yè)務(wù)組件需要在common組件中聲明對外開放的接口,那有沒有什么方式可以做到完全解耦呢?目前還沒有發(fā)現(xiàn)更好的辦法。。。
- 提高開發(fā)效率:依賴解耦這一優(yōu)勢,團隊成員可以只專注于自己負責(zé)的組件,開發(fā)效率更高;而且,組件開發(fā)過程中只需編譯自身的module,這樣大大縮短了編譯時長,避免了漫長的等待編譯局面。
- 結(jié)構(gòu)清晰:在業(yè)務(wù)組件明確拆分的前提下,項目結(jié)構(gòu)變的異常清晰,非常方便全局掌控。
喜歡點擊+關(guān)注哦
