分析某OTA平臺Android客戶端建立模擬架構(gòu)
一、工作回顧
一年中根據(jù)新酒店業(yè)務(wù)開發(fā)不斷的對架構(gòu)進(jìn)行優(yōu)化迭代肪凛,同時(shí)也綜合了“XXX客戶端”現(xiàn)有的業(yè)務(wù)場景以及現(xiàn)有項(xiàng)目結(jié)構(gòu)進(jìn)行分析,最終建立了AndroidModuleDevPro架構(gòu)和開發(fā)模式辽社,項(xiàng)目基于MVPArms伟墙、組件化開發(fā)的基礎(chǔ)上,用來解決因?yàn)闃I(yè)務(wù)的增加導(dǎo)致項(xiàng)目的體積變得龐大而難以維護(hù)滴铅,同時(shí)也增強(qiáng)了團(tuán)隊(duì)協(xié)作的靈活性戳葵,可以更快捷有效的開發(fā)、測試汉匙、迭代拱烁。
二、組件化
基于現(xiàn)在的“XXX戶端”分析噩翠,現(xiàn)有業(yè)務(wù)有:機(jī)票戏自、酒店、火車票伤锚、貴賓廳擅笔、專車、門票见芹、管家金卡剂娄、代換登機(jī)牌、安檢通道玄呛、延誤險(xiǎn)理賠阅懦、會員商城、行程管理徘铝、優(yōu)秀員工耳胎、航班動態(tài)、個(gè)人中心惕它、我的錢包怕午、常用信息、消息中心淹魄、訂單中心郁惜、特價(jià)機(jī)票、特價(jià)酒店等甲锡;
從上述功能可以看出我們的APP的業(yè)務(wù)覆蓋已經(jīng)非常的全面兆蕉,所以這也導(dǎo)致我們項(xiàng)目的代碼量的猛增羽戒,由于架構(gòu)設(shè)計(jì)沒有跟上業(yè)務(wù)的增長,導(dǎo)致了我們現(xiàn)有的幾個(gè)問題:采用單一項(xiàng)目結(jié)構(gòu)項(xiàng)目代碼冗余虎韵、模塊間耦合度高不易于迭代維護(hù)易稠、開發(fā)調(diào)試運(yùn)行時(shí)間漫長、人員的分配無法隨機(jī)調(diào)動且代碼閱讀成本較高包蓝、無法單一業(yè)務(wù)模塊測試導(dǎo)致測試流程繁瑣等驶社;
所以如果能有一種開發(fā)模式可以將每一個(gè)業(yè)務(wù)單做一個(gè)APP運(yùn)行豈不是可以節(jié)省不少構(gòu)建的時(shí)間,并且還可以使得項(xiàng)目結(jié)構(gòu)更加的清晰测萎。
三亡电、組件拆解
基于“問題描述”我們不難看出,“客戶端”每一個(gè)業(yè)務(wù)單獨(dú)抽離都可以作為一個(gè)完整的APP獨(dú)立開發(fā)運(yùn)行硅瞧,所以我們通過這個(gè)思路先對項(xiàng)目進(jìn)行較大粒度的劃分逊抡,劃分后的效果如下:
配圖說明:
lib_common:架構(gòu)基礎(chǔ),提供公用的架構(gòu)零酪、工具類等,不參與任何業(yè)務(wù)處理拇勃;
lib_protobuf:數(shù)據(jù)實(shí)體四苇,提供數(shù)據(jù)實(shí)體、不參與任何業(yè)務(wù)處理方咆;
module_app:宿主月腋,不參與任何業(yè)務(wù)處理;
module_main:主框架瓣赂,提供應(yīng)用的基礎(chǔ)條件榆骚,一般只用包含、啟動頁煌集、主視圖框架等妓肢;
module_user:用戶組件,用戶相關(guān)信息苫纤,比如:登陸碉钠、注冊、修改信息卷拘、常用信息等喊废,同時(shí)也會向外部提供公用數(shù)據(jù)的獲取栗弟;
module_hotel:酒店組件污筷,酒店的查詢、列表乍赫、詳情瓣蛀、下單陆蟆、訂單管理等;
module_flight_info:航班動態(tài)揪惦,查詢遍搞、列表、詳情器腋;
module_tickets:門票組件溪猿,查詢、列表纫塌、詳情诊县、下單、訂單管理措左;
module_air_ticket:機(jī)票組件依痊,查詢、列表怎披、詳情胸嘁、下單、訂單管理凉逛;
module_train_ticket:火車票組件性宏,查詢、列表状飞、詳情毫胜、下單、訂單管理诬辈;
module_insurance:延誤險(xiǎn)理賠組件酵使,查詢、列表焙糟、詳情口渔、下單、訂單管理穿撮;
module_bording_pass:代換登機(jī)牌搓劫,查詢、列表混巧、詳情枪向、下單、訂單管理咧党;
module_message:消息中心秘蛔,查詢、列表、詳情深员、下單负蠕、訂單管理;
根據(jù)業(yè)務(wù)動態(tài)添加其它
當(dāng)我們將項(xiàng)目拆分為上述結(jié)構(gòu)之后倦畅,就可以針對每一個(gè)組件進(jìn)行單獨(dú)的調(diào)試遮糖、開發(fā)、迭代升級等操作叠赐,并且不會影響到其他的模塊欲账,那么在實(shí)際的開發(fā)過程中怎么去實(shí)現(xiàn)呢?
Android Studio 是基于Gradle構(gòu)建的項(xiàng)目芭概,Android常用的Module類型有Android Library赛不、Phone&Table Module、Java Library這三個(gè)罢洲,可以進(jìn)行單獨(dú)運(yùn)行的模式為Phone&Table Module踢故、其余的兩個(gè)模式可以作為引入關(guān)系引入到項(xiàng)目中,所以我們基于這個(gè)特性對項(xiàng)目的Module類型進(jìn)行動態(tài)的切換從而達(dá)到組件化中“集成模式”&“組建模式”的切換效果惹苗,思路理清了那么就直接上代碼:
每個(gè)要作為組件模塊的build.gradle文件都要添加下面的代碼:
//isModule.toBoolean()是我們在gradle.properties文件中聲明的一個(gè)變量
if (isModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
gradle.properties文件
# 每次更改“isModule”的值后殿较,需要點(diǎn)擊 "Sync Project" 按鈕
# isModule是“集成開發(fā)模式”和“組件開發(fā)模式”的切換開關(guān)
isModule=false
最后打包發(fā)布的時(shí)候我們肯定是整包發(fā)布,所以作為宿主的module_app要將需要整合的組件引入進(jìn)來桩蓉,引用關(guān)系使用下面的代碼:
module_app組件中的build.gradle文件:
//依賴設(shè)置
dependencies {
//引入基礎(chǔ)的開發(fā)包
implementation project(':lib_base')
//isModule.toBoolean()是我們在gradle.properties文件中聲明
if (!isModule.toBoolean()) {
implementation project(':module_main')
implementation project(':module_hotel')
implementation project(':module_order')
implementation project(':module_user')
//………其它的你想引入進(jìn)來的模塊
}
}
這里有一個(gè)點(diǎn)需要注意斜脂,在Gradle 4.X之后依賴的dependencies關(guān)鍵字發(fā)生了變化:
api:完全等同于compile指令,沒區(qū)別触机,你將所有的compile改成api,完全沒有錯(cuò)玷或。
implementation:這個(gè)指令的特點(diǎn)就是儡首,對于使用了該命令編譯的依賴,對該項(xiàng)目有依賴的項(xiàng)目將無法訪問到使用該命令編譯的依賴中的任何程序偏友,也就是將該依賴隱藏在內(nèi)部蔬胯,而不對外部公開。
舉個(gè)例子:
比如我在一個(gè)libiary中使用implementation依賴了gson庫位他,然后我的主項(xiàng)目依賴了libiary氛濒,那么,我的主項(xiàng)目就無法訪問gson庫中的方法鹅髓。這樣的好處是編譯速度會加快舞竿,推薦使用implementation的方式去依賴,如果你需要提供給外部訪問窿冯,那么就使用api依賴即可
在Google IO 相關(guān)話題的中提到了一個(gè)建議骗奖,就是依賴首先應(yīng)該設(shè)置為implementation的,如果沒有錯(cuò),那就用implementation执桌,如果有錯(cuò)鄙皇,那么使用api指令,這樣會使編譯速度增快仰挣。
所以在組件build.gradle中我們推薦下面的配置:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':lib_base')
}
解決了module類型的動態(tài)配置之后我們將會面臨另一個(gè)問題伴逸,就是“組件模式”“集成模式”下的配置切換問題,為什么要說這個(gè)問題呢膘壶?原因就是“組件模式”下我們會模擬很多的數(shù)據(jù)错蝴,讓當(dāng)前的組件處于仿真環(huán)境下,方便我們的調(diào)試開發(fā)香椎。但是當(dāng)我們要進(jìn)行“集成模式”開發(fā)的時(shí)候這些配置信息都是無用的漱竖,甚至?xí)绊懳覀兊募蓸?gòu)建,所以就要“組件模式”“集成模式”下的配置進(jìn)行分離處理畜伐,具體怎么做馍惹?配置如下:
在組件的build.gradle文件中
android {
//重新設(shè)置資源指向
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
//組件模式下將組件使用的java目錄導(dǎo)入進(jìn)來
java.srcDirs 'src/main/module/debug', 'src/main/java'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//集成開發(fā)模式只保留原有的java目錄
java.srcDirs 'src/main/java'
}
}
}
}
組件內(nèi)部結(jié)構(gòu)是下面這樣的:
經(jīng)過上面的幾個(gè)步驟項(xiàng)目組件化的結(jié)構(gòu)基本級已經(jīng)確立下來了,接下來我們就可以為所欲為的添加新的組件到我們的項(xiàng)目中玛界,而不必?fù)?dān)心這個(gè)組件對別的組件的影響万矾。
四、組件通信
組件拆解完成之項(xiàng)目看起來確實(shí)清爽了不少慎框,但是噩夢也隨之而來良狈。由于我們的組件是相互獨(dú)立的,所以……頁面之間怎么傳值笨枯?頁面怎么跳轉(zhuǎn)薪丁?Fragment怎么加載?模塊之間怎么通信馅精?等等一些問題隨之而來严嗜。
Activity跳轉(zhuǎn)的問題?
在不破壞組件化結(jié)構(gòu)的前提下洲敢,我們首先想到的應(yīng)該是隱式跳轉(zhuǎn)漫玄,通過在AndroidMainfest.xml文件中注冊過濾器
<activity android:name=".CategoryActivity" >
<intent-filter>
<action android:name="customer_action_here" />
</intent-filter>
</activity>
在代碼中使用下面代碼進(jìn)行跳轉(zhuǎn)操作
//創(chuàng)建一個(gè)隱式的 Intent 對象:Category 類別
Intent intent = new Intent();
intent.setAction("customer_action_here");
startActivity(intent);
看似我們好像友好的解決了這個(gè)問題,先放著我們進(jìn)行下一個(gè)問題压彭。
Fragment怎么加載睦优?
主框架UI一般都是聚合了多個(gè)組件的內(nèi)部Fragment進(jìn)行展示的,這個(gè)時(shí)候又當(dāng)如何使用呢壮不?
結(jié)合Java反射機(jī)制汗盘,可以直接反射出類然后創(chuàng)建對象來使用,由于代碼量比較大這里就不展開講解询一。但是問題好像是解決了衡未。
模塊間通信尸执?
用戶信息我們在很多的頁面都會用到,但是我們建立了User組件缓醋,信息被隔離到了User組件內(nèi)部如失,如果別的組件想要使用我們的User組件內(nèi)的信息怎么辦?
比較簡單的就是將這種數(shù)據(jù)操作封裝到lib_base中這樣所有的組件就都可以直接使用了送粱,但是這樣無形中增加了lib_base對業(yè)務(wù)的管理這并不是最好的實(shí)現(xiàn)褪贵,那么還有什么方式可以解決這個(gè)問題呢?
可以在lib_base中建立業(yè)務(wù)接口UserInfoService抗俄,同時(shí)在里面編寫模板方法方法:
public interface UserInfoService {
boolean isLogin();
UserInfo getUserInfo();
String getOpenId();
}
在User組件中進(jìn)行實(shí)現(xiàn):
public class UserInfoServiceImpl implements UserInfoService {
@Override
public boolean isLogin() {
return SpUserInfo.isLogin();
}
@Override
public UserInfo getUserInfo() {
return SpUserInfo.getUserInfo();
}
@Override
public String getOpenId() {
return SpUserInfo.getOpenId();
}
}
然后再使用java的反射機(jī)制對實(shí)現(xiàn)了UserInfoService接口的類進(jìn)行實(shí)例化脆丁,在調(diào)用方使用父類接口UserInfoService引用,由于接口直接引用的是一個(gè)實(shí)例化后的對象动雹,這個(gè)時(shí)候我們是直接可以通過引用關(guān)系調(diào)用到內(nèi)部方法的槽卫。從而模塊間數(shù)據(jù)通訊的問題也得到了解決。
問題是解決了胰蝠,那么實(shí)際開發(fā)好不好用呢歼培?
總結(jié)一下,頁面跳轉(zhuǎn)都要顯示的指定一個(gè)隱式跳轉(zhuǎn)的意圖茸塞,清單文件還要去注冊對應(yīng)的攔截過濾躲庄。而如果遇到了Fragment就更加麻煩,首先要去反射指定的Fragment钾虐,然后才能使用噪窘。接著就是模塊間的數(shù)據(jù)通訊,也是要先建立一個(gè)頂層接口效扫,然后要去手動的反射進(jìn)行實(shí)例化倔监,接著才能使用。
上述的操作每次使用都要進(jìn)行一編菌仁,無形中增加了代碼的冗余浩习,且維護(hù)性極差,所以我們就要將這些操作全都封裝到一個(gè)工具框架中掘托,使用的時(shí)候就幾代碼就可以完成響應(yīng)的操作。
原理我們了解了籍嘹,封裝的原理我們也想明白了闪盔,這里也是為了不重復(fù)的造輪子就直接使用阿里巴巴開源出來的ARouter進(jìn)行解耦后的頁面跳轉(zhuǎn)、Fragment初始化加載辱士、頁面?zhèn)髦底⑷肜嵯啤⒔M件件通信。
使用方式這里不贅述颂碘,請參見官方地址ARouter异赫。
五、ARouter遇坑
經(jīng)過了上面的操作,組件化的雛形終于建立起來了塔拳,那么是不是就可以愉快的擼代碼呢鼠证?
如果你認(rèn)為可以的話那么我只能說,少年你還是太天真靠抑。
由于解決組件通信問題使用的是ARouter解決方案量九,它可以很好的幫我們生成一些中間代碼,讓我們可以用最少代碼量實(shí)現(xiàn)功能颂碧,但是他有一個(gè)弊端荠列,就是ARouter為我們生成的代碼是統(tǒng)一的一個(gè)路徑“com.alibaba.android.arouter.routes”那么這個(gè)對我們有什么影響呢?
下面分析一下這個(gè)對我們有什么影響:
對比上面兩個(gè)組件構(gòu)建后的代碼中载城,ARouter幫我們生成了幾個(gè)java文件:
ARouter$$Group$$組名
ARouter$$Providers$$組名
ARouter$$Root$$組名
如果說我們要進(jìn)行開發(fā)規(guī)范制定肌似,比如Activity的都放在act中,F(xiàn)ragment都放在fgt中诉瓦,Service都放在service中川队,然后將這些規(guī)范定義在lib_base中的一個(gè)常量ARouterPath類中進(jìn)行管理,然后再使用的時(shí)候直接使用ARouterPath.XXXX進(jìn)行調(diào)用垦搬,在實(shí)際調(diào)試中會出現(xiàn)一個(gè)效果就是如下圖:
可以看到ARouter幫我們生成了類名一模一樣的兩個(gè)java文件呼寸,這兩個(gè)文件分別位于不同組件的同包名下,所以當(dāng)我們進(jìn)行構(gòu)建項(xiàng)目的時(shí)候這兩個(gè)文件是會發(fā)生沖突的猴贰,其中一個(gè)會覆蓋掉另一個(gè)对雪,導(dǎo)致應(yīng)用調(diào)用異常,所以這個(gè)時(shí)候建立規(guī)則的時(shí)候建議采用組件名稱作為組名的后綴米绕,這樣可以有效的避免文件覆蓋帶來的錯(cuò)誤瑟捣。
六、MVP模式介紹
全稱Model - View - Presenter 栅干,MVP 是從經(jīng)典的模式 MVC 模式演變來的迈套,它們的基本思想有相同的地方:MVC的Controller層與MVP中的Presenter層負(fù)責(zé)邏輯的處理,Model提供數(shù)據(jù)碱鳞,View負(fù)責(zé)顯示桑李。
之前學(xué)的是后端開發(fā),第一個(gè)接觸的就是MVC模式窿给,用起來很舒爽贵白,但是做了Android開發(fā)之后使用同樣的MVC模式進(jìn)行嵌套會發(fā)現(xiàn),一切都變的不那么的美好了崩泡,因?yàn)橐晥D層Activity/Fragment既充當(dāng)了View的視圖角色禁荒,又擔(dān)任了Controller的角色,導(dǎo)致Activity/Fragment的代碼量猛增角撞,且難以閱讀和維護(hù)呛伴。
作為新的設(shè)計(jì)模式勃痴,MVP與MVC有這一個(gè)重大的區(qū)別:MVP中的View并不直接使用Model,它們之間通信是公國Presenter(MVC中的Controller)來進(jìn)行的热康,所有的交互都發(fā)生在Presenter內(nèi)部沛申,而在MVC中的View會直接從Model中讀取數(shù)據(jù)而不是通過Controller。
在MVP中褐隆,Presenter完全把Model和View進(jìn)行了分離污它,主要的程序邏輯在Presenter里實(shí)現(xiàn)。而且庶弃,Presenter與具體的View是沒有直接關(guān)聯(lián)的衫贬,而是通過定義好的接口進(jìn)行交互,從而使得在變更View時(shí)候可以保持Presenter的重用歇攻。
甚至可以在Model和View都沒有完成的時(shí)候就可以通過編寫Mock Object(即實(shí)現(xiàn)了Model和View的接口固惯,但是沒有具體的內(nèi)容)來測試Presenter的邏輯。
所以在MVP里缴守,應(yīng)用程序的邏輯主要在Presenter中實(shí)現(xiàn)葬毫,其中View是相對獨(dú)立一層。因此我們可以采用Presenter First的設(shè)計(jì)模式屡穗,就是根據(jù)需求先設(shè)計(jì)和開發(fā)Presenter贴捡。在這個(gè)過程中,View可以使很簡單村砂,能夠把信息顯示清除即可烂斋。在后面,根據(jù)具體的頁面需求再調(diào)整View的樣式础废,而對Presenter沒有任何影響汛骂。
如果UI比較復(fù)雜,而且相關(guān)的顯示邏輯還跟Model有關(guān)系评腺,就可以在View和Presenter之間放置一個(gè)Adapter帘瞭。由這個(gè)Adapter來訪問Model和View,避免兩者之間的關(guān)聯(lián)蒿讥。
在MVP模式里蝶念,View只應(yīng)該有簡單的Set/Get的方法,用戶輸入和設(shè)置頁面顯示的內(nèi)容芋绸,除此就不應(yīng)該有更多的內(nèi)容媒殉,決不允許直接訪問Model。這也是與MVC很大的不同之處侥钳。
優(yōu)點(diǎn):
1适袜、模型與視圖完全分離柄错,我們可以修改視圖而不影響模型舷夺。
2苦酱、可以更高效地使用模型,因?yàn)樗械慕换ザ及l(fā)生在一個(gè)地方给猾,Presenter內(nèi)部疫萤。
3、可以將一個(gè)Presenter用于多個(gè)視圖敢伸,而不需要改變Presenter的邏輯扯饶,這個(gè)特性非常有用因?yàn)橐晥D的變化是頻繁的。(建議僅限于同一個(gè)視圖的使用池颈,跨視圖會出現(xiàn)重寫不必要的接口方法的弊端)
4尾序、可以脫離View來進(jìn)行邏輯測試;
缺點(diǎn):
由于對視圖的渲染放在了Presenter中躯砰,所以視圖與Presenter的交互過于頻繁每币,且緊密聯(lián)系,所以一旦視圖發(fā)生變更琢歇,那么Presenter也要變更兰怠。比如說,原來呈現(xiàn)酒店詳情的視圖現(xiàn)在要呈現(xiàn)酒店周邊餐飲了李茫,那么Presenter也要變更揭保。
//TODO MVP的構(gòu)建模式,結(jié)合Dagger進(jìn)行處理等