在進(jìn)行app組件化之前我們要明白什么是組件化?為什么要組件化?
什么是組件化?為什么要組件化?
????在項(xiàng)目的體系結(jié)構(gòu),代碼量,功能,邏輯等不斷的增長(zhǎng)之后,項(xiàng)目的編譯,開發(fā)的協(xié)作,效率等都會(huì)變得毫無體驗(yàn)。所以我們就要相處一個(gè)辦法來解決這個(gè)事情。一般來說會(huì)有如下幾種思路
1,組件化
2,插件化
3,模塊化
????如果對(duì)于app的動(dòng)態(tài)化,體積等有比較大的考量則插件化是不錯(cuò)的選擇。但是一般來說市面上除非像淘寶這種超級(jí)app,有這強(qiáng)大的技術(shù)底蘊(yùn)的團(tuán)隊(duì)的app,其實(shí)大部分的app是不需要必須使用插件化的技術(shù)的。有些公司其實(shí)是完全跟風(fēng)罷了。而且在安卓9.0之后插件化的提要其實(shí)并不友好,所以一般非必須的情況下并不臺(tái)推薦使用插件化。
????模塊化我理解則是將組件化更加細(xì)粒度的進(jìn)行拆分,比如可能會(huì)將一個(gè)輸入欄作為一個(gè)模塊進(jìn)行復(fù)用,比如QQ的聊天輸入,或者微信的聊天輸入等類似的。這樣的場(chǎng)景架構(gòu)其實(shí)也是非一般公司可為。
????當(dāng)然你可以將三者組合起來使用,組件化的同時(shí)實(shí)現(xiàn)插件化,再將各個(gè)業(yè)務(wù)模塊完全抽離,然后還有各種動(dòng)態(tài)化,部分組件的跨平臺(tái)等等。。
????但是架構(gòu)是需要一步一步的進(jìn)化的,如果要一口氣的整一個(gè)巨無霸出來,那只能將自己耗死,即使整出來,也一定會(huì)被自己憋死,因?yàn)槌缘奶柫恕?/p>
????所以對(duì)于一個(gè)業(yè)務(wù)成長(zhǎng)飛速,但體量又沒有過于復(fù)雜的app來說,組件化是最好的架構(gòu)升級(jí)方案,因?yàn)樗銐虻母咝В?jí)之后的付出與收獲成正比。
組件化要解決的問題
????在進(jìn)行組件化之前我們要思考為什么要組件化,組件化能為我們解決什么要的問題。
????想象一個(gè)場(chǎng)景,我們?cè)谶M(jìn)行獨(dú)立開發(fā)的時(shí)候,整個(gè)代碼完全就是我們自己的世界,甚至我們都不用進(jìn)行g(shù)it pull 的操作,只需要git push即可,毫無代碼沖突風(fēng)險(xiǎn)。
????然而有一天小明來到了公司和你一起開發(fā),這回你需要熟悉git的各種操作了,如何merge,倆人商量著如何不互相改代碼。不過開發(fā)體驗(yàn)還是可以接受的。
????又過了一段時(shí)間公司獲得了馬云100個(gè)億的投資,老板飄了。老板決定每個(gè)開發(fā)的崗位增加二十個(gè)人。當(dāng)然因?yàn)轳R爸爸投資了公司,公司自然不能差,業(yè)務(wù)飛漲,這二十個(gè)人自然也沒有閑下來,每人都負(fù)責(zé)的一個(gè)小功能,比如小明同學(xué)在安心的做著商品詳情相關(guān)的功能。大家每人維護(hù)著一個(gè)包,其樂融融。
????但是你作為一個(gè)公司元老級(jí)別的老鳥很快發(fā)現(xiàn)了一個(gè)問題,而是多個(gè)兄弟在同時(shí)在一個(gè)包下工作,有一天你突然發(fā)現(xiàn)util包下有timeUtil,dateUtil等幾個(gè)看起來很類似的工具類。并且你發(fā)現(xiàn)小明同學(xué)寫代碼的時(shí)候經(jīng)常會(huì)不小心動(dòng)了其他同學(xué)的代碼,導(dǎo)致小明同學(xué)的人生安全產(chǎn)生了很大的隱患。
????然后你發(fā)現(xiàn)一個(gè)更重要的問題,自己每次改一個(gè)小功能的時(shí)候,竟然都要花上一把王者榮耀的時(shí)間去等待app跑起來,偶爾還會(huì)編譯失敗。測(cè)試同學(xué)最近的臟話也月來越多。。。
????公司代碼混亂不堪,開發(fā)氛圍壓抑無比,看來解決問題迫在眉睫了!你發(fā)現(xiàn)了以下幾點(diǎn)需要解決的問題:
1,公共組件的提取
2,每個(gè)人維護(hù)的模塊內(nèi)容,不沖突,不會(huì)互相傷害,獨(dú)立調(diào)試
3,解決編譯速度問題
4,模塊跳轉(zhuǎn),通信等
如何組件化
???? 先看一張圖

???? 在整個(gè)結(jié)構(gòu)的最上層就是app模塊,其實(shí)他和module沒有太大區(qū)別,本質(zhì)上也是一個(gè)module,只不過是一個(gè)殼module,使用他來做一些集成其他子module的操作與分發(fā)。
???? 而在結(jié)構(gòu)的第二層便是各種module,比如order模塊,login模塊等等,每個(gè)模塊項(xiàng)目隔離,相互獨(dú)立。
???? 在結(jié)構(gòu)的第三層是base,router,bus共同模塊,按需集成。這里我沒有把brouter和bus放到base中。
???? 在結(jié)構(gòu)的最下層則是我們公司各種通用的基礎(chǔ)設(shè)施,比如http模塊,可能有多個(gè)項(xiàng)目使用,可能是第三方庫(kù)(okhttp),或者公司基礎(chǔ)設(shè)施的團(tuán)隊(duì)自己維護(hù)的libray,一般情況都是通過aar來引入。當(dāng)然這里為了直觀,我把這些以源碼的形式做到工程中。
這里有倆個(gè)東西是必備的,router和bus
Router
市面上有很router庫(kù),功能大同小異,這里推薦阿里的Arouter
https://github.com/alibaba/ARouter
用法不多介紹,很簡(jiǎn)單
說下使用router的必要性和好處
1,模塊之間的activity是不能通過原生去跳轉(zhuǎn)的,這是router首要解決的問題。
2,router沒有intent傳參大小限制的問題。
3,router有強(qiáng)大的攔截器和降級(jí)
4,router可以是模塊之間通信的重要手段,如服務(wù)的發(fā)現(xiàn)等。
5,對(duì)組件化的埋點(diǎn)和統(tǒng)計(jì)等有著很好的幫助。
。。。
Bus
bus主要解決的是組件之間通信的問題,替代廣播等原生方案,bus有很多中,eventbus,rxbus, livedataBus等。。
這里我簡(jiǎn)單的寫了個(gè)livedatabus來作為組件化的bus方案。livedatabus的好處是生命周期的感知,重要的是簡(jiǎn)單。避免了eventbus的各種注解,迷之傳遞。
實(shí)踐
首先砍下我寫的demo結(jié)構(gòu)圖

這里我簡(jiǎn)單的寫了一些gradle腳本來配置工程

一,module的動(dòng)態(tài)引用
app的gradle文件
apply plugin: 'com.android.application'
apply from: "$rootProject.projectDir/buildScript/main_build.gradle"
android {
defaultConfig {
versionCode rootProject.versionCode
versionName rootProject.versionName
}
}
main_build.gradle文件
在這個(gè)文件中,我們讀取一個(gè)gradle的參數(shù)來識(shí)別殼工程需要引用的module,然后動(dòng)態(tài)的引用,這樣就避免了我們?yōu)榱霜?dú)立的調(diào)試需要不停的修改module的類型。然后在編譯期間把個(gè)參數(shù)插入到string中方便我們?cè)诖a中使用。
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/buildScript/buildTypes/appBuildTypes.gradle"
apply from: "$rootProject.projectDir/buildScript/buildTypes/appProductFlavors.gradle"
apply from: "$rootProject.projectDir/buildScript/router/arouter.gradle"
apply from: "$rootProject.projectDir/buildScript/module_common_build.gradle"
def getDependencyModule() {
return project.properties.get("dependencyModule")
}
def modules = getDependencyModule().split(',')
android{
buildTypes {
release {
resValue "string", "modules", getDependencyModule()
}
debug {
resValue "string", "modules", getDependencyModule()
}
}
}
modules.each {
module ->
print(module)
project.dependencies.add("implementation", project(':' + module))
}
然后我們?cè)趃radle的配置文件中配置一個(gè)參數(shù)來引用相應(yīng)的module
每次我們需要修改引用的module時(shí)就改一下這里的配置即可。
#需要依賴的moudle
dependencyModule=moudle1,moudle3
二,module的applaction初始化
我們每個(gè)module可能除了一些公有的庫(kù)之外,會(huì)引入一些只有自己會(huì)使用的庫(kù),我們無法在app的applaction中作初始化的操作,所以我們必須要為每個(gè)module都作初始化。
這里我們?cè)趓outer模塊中協(xié)理一個(gè)applaoction初始化的服務(wù)。
interface IAppInit : IProvider {
fun initApp(applaction: Application)
}
然后需要初始化的module去實(shí)現(xiàn)這個(gè)接口,并把路由的地址暴露出來。
比如這里是module1的初始化實(shí)現(xiàn)。
@Route(path = AppModules.module1AppInit)
class Module1Applaction : IAppInit {
override fun initApp(applaction: Application) {
Log.e("Module1Applaction","initApp")
}
override fun init(context: Context) {
}
}
然后我們AppModules這個(gè)里面配置我們的路由地址。
object AppModules {
const val module1AppInit = "/module1/appInit"
const val module2AppInit = "/module2/appInit"
const val module3AppInit = "/module3/appInit"
fun getModulePath(module: String): String? {
return when (module) {
"moudle1" -> module1AppInit
"moudle2" -> module2AppInit
"moudle3" -> module3AppInit
else -> null
}
}
}
接著我們就可以在我們的app的主applaiction中去根據(jù)我們配置的modules去獲取每個(gè)module相應(yīng)的初始化實(shí)現(xiàn)進(jìn)行初始化,這樣就實(shí)現(xiàn)了每個(gè)module都可以使用自己的初始化方案了。這是我們要解決的第二個(gè)問題。
private fun initModules() {
val modules = getString(R.string.modules).split(",")
modules.forEach { module ->
val modulePath = AppModules.getModulePath(module)
if (modulePath != null) {
val navigation = ARouter.getInstance().build(modulePath).navigation()
if (navigation != null && navigation is IAppInit) {
navigation.initApp(this)
}
}
}
}
三,模塊的獨(dú)立調(diào)試
同樣我們?yōu)槊總€(gè)模塊引入下面的腳本
apply from: "$rootProject.projectDir/buildScript/module_build.gradle"
android {
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
versionCode rootProject.versionCode
versionName rootProject.versionName
}
}
module_build.gradle腳本
在這個(gè)腳本中,我們同樣根據(jù)gradle的配置參數(shù)來動(dòng)態(tài)的配置
如果配置的參數(shù)中有我們的module,那么就為modlue引入:apply plugin: 'com.android.library'使其成為一個(gè)libray。反之則引入 apply plugin: 'com.android.application' 時(shí)期成為一個(gè)可以獨(dú)立調(diào)試的app。當(dāng)然這樣還是不能夠進(jìn)行調(diào)試的,我們還需要?jiǎng)討B(tài)的配置AndroidManifest的路徑。
def getDependencyModule() {
return project.properties.get("dependencyModule")
}
def modules = getDependencyModule().split(',')
def contains = modules.contains(project.getName())
if (contains) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
delete project.buildDir
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/buildScript/router/arouter.gradle"
apply from: "$rootProject.projectDir/buildScript/buildTypes/appBuildTypes.gradle"
apply from: "$rootProject.projectDir/buildScript/module_common_build.gradle"
android {
sourceSets {
main {
if (contains) {
manifest.srcFile 'src/main/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
}
}
}
}

如上圖,我們?cè)趍ain的同級(jí)別配置一個(gè)debug,在module沒有被引入的時(shí)候我們就使用這里面的AndroidManifest,我們?cè)贏ndroidManifest中配置我們的applaction,以及啟動(dòng)的activity。之后我們就可以直接進(jìn)行調(diào)試啟動(dòng)了。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xiaofeiluo.moudle1">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:name=".debug.Module1Applaction"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<activity android:name=".Module1HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
如下圖便是沒有將module引入app中的時(shí)候

四,組件之間通信的問題
組件和組件之間是相互獨(dú)立的,想要進(jìn)行通信只能通過下層的共同模塊來進(jìn)行。一共有倆種方案
方案一:面向接口的通信,既是服務(wù)暴露
這種方案一般來說是我們一個(gè)模塊想要獲取另一個(gè)模塊的數(shù)據(jù),或者說一個(gè)模塊做了一個(gè)服務(wù)需要share給其他的模塊我們可以使用這中方案。
比如下面的例子
我們需要獲取module3中的用戶數(shù)據(jù),那么我們需要在router中定義一個(gè)接口
interface IUserInfoService : IProvider {
fun getUserName(callback: (name: String) -> Unit)
fun getUserAge(callback: (age: String) -> Unit)
fun getUserSchool(callback: (school: String) -> Unit)
}
然后我們要在module3中去實(shí)現(xiàn)這個(gè)接口,并且把路由地址配置到這個(gè)實(shí)現(xiàn)類
@Route(path = Module3RouterPath.UserInfoService)
class UserInfoService : IUserInfoService {
private var name: String? = null
private var age: String? = null
private var school: String? = null
private var handler: Handler? = null
override fun getUserName(callback: (name: String) -> Unit) {
if (TextUtils.isEmpty(name)) {
thread {
Thread.sleep(1000);
name = "張三"
handler?.post {
callback.invoke(name!!)
}
}
} else {
callback.invoke(name!!)
}
}
override fun getUserAge(callback: (age: String) -> Unit) {
if (TextUtils.isEmpty(age)) {
thread {
Thread.sleep(1000);
age = "10"
handler?.post {
callback.invoke(age!!)
}
}
} else {
callback.invoke(age!!)
}
}
override fun getUserSchool(callback: (school: String) -> Unit) {
if (TextUtils.isEmpty(school)) {
thread {
Thread.sleep(1000);
school = "清華大學(xué)"
handler?.post {
callback.invoke(school!!)
}
}
} else {
callback.invoke(school!!)
}
}
override fun init(context: Context?) {
handler = Handler(Looper.getMainLooper())
}
}
然后在module1中我們就可以獲取這個(gè)服務(wù)去使用了
getName.setOnClickListener {
val userInfo = ARouter.getInstance().build(Module3RouterPath.UserInfoService).navigation()
userInfo?.let {
if (it is IUserInfoService) {
it.getUserName {
name.text = it
}
}
}
}
方案二,bus
bus的方案我們一般用來主動(dòng)的去監(jiān)聽一些變化的發(fā)生,而不想接口是去獲取,bus是主要解決接受的通信。
這里我簡(jiǎn)單寫了個(gè)livedatabus來作為總線,然后我們面向具體的數(shù)據(jù)模型進(jìn)行監(jiān)聽。
這里我用一個(gè)叫做event的注解標(biāo)識(shí)這是一個(gè)可以被傳遞的event,注解中的內(nèi)容是這個(gè)evnet對(duì)應(yīng)的key,
bus的原理就不多作介紹。
@Event("UserEvent")
class UserEvent(var newName: String)
然后我們就可以去監(jiān)聽變化了
接受消息
BusManager.call(UserEvent::class).observe(this) {
name.text = it.newName
}
發(fā)送消息
updateName.setOnClickListener {
BusManager.postEvent(UserEvent("李四"))
}
好了,到這里我們就完成了組件之間通信的方案。
最后說幾個(gè)問題。
1,為什么bus,router,base要分開維護(hù)?
這里我覺得base作為一個(gè)功能是每個(gè)模塊都需要的,一般來說是由一個(gè)伙計(jì)去維護(hù)的,這個(gè)東西一般來說也是以一種aar的形式去集成的。而route和bus則不同,他們是需要每個(gè)模塊的小伙伴自己去維護(hù)的,所以物品們要在router和bus之內(nèi)去明確的分包,所以改動(dòng)可能會(huì)很頻繁,我們避免不小心碰觸到base中的核心內(nèi)容,最好把這些配置類的東西分離出來獨(dú)立維護(hù)。還有一種辦法是,這倆個(gè)東西我們統(tǒng)一由一個(gè)小伙伴維護(hù),其他的模塊想要在里面添加服務(wù),或者路由,那么就需要統(tǒng)一的告訴這個(gè)小伙伴并且配備相應(yīng)的說明文檔,這個(gè)小伙伴再進(jìn)行統(tǒng)一的審核。
2,關(guān)于工程的一些配置
這里我么可以利用gradle把一些沒必要重復(fù)的配置抽離出來,同是也可以統(tǒng)一的進(jìn)行管理,比如一些每個(gè)模塊都需要的libray,junit,apt,kpt這些的可以拿出去。然后關(guān)于版本的話我們可以每個(gè)模塊進(jìn)行獨(dú)立的維護(hù),因?yàn)槲覀兏鱾€(gè)模塊的versionCode不一定一樣,大部分情況下,我們是需要CI進(jìn)行配合來自動(dòng)化構(gòu)建的,所以模塊一般也是需要發(fā)aar來引用。模塊之間也有獨(dú)立的倉(cāng)庫(kù),一般我們會(huì)用submodule的形式引入自己開發(fā)的模塊,別人的模塊我們則需要使用aar的形式來引用。所以可以根據(jù)實(shí)際的需求來寫一些gradle的腳本來擴(kuò)展我們的工程。比如說我們加個(gè)配置來決定使用源碼引用還是aar引用等。
這個(gè)只是最簡(jiǎn)單的一系列配置,解決了基本的組件化方案的思路,實(shí)際情況可能要復(fù)雜很多,但是基本的問題已經(jīng)解決。