在 《也談Android應(yīng)用架構(gòu)》 和 《Jetpack之Lifecycle、LiveData及ViewModel是如何讓架構(gòu)起飛的》 兩篇文章中,我們?cè)敿?xì)論述了MVC、MVP、MVVM架構(gòu)的思想、優(yōu)缺點(diǎn)以及使用注意事項(xiàng),并闡述了借助Jetpack強(qiáng)大的生命周期管控能力解決架構(gòu)“本地化”的問(wèn)題。但沒有實(shí)踐的論述不僅不直觀,也應(yīng)了那句Talk is cheap, show me the code.的經(jīng)典名言,因此這個(gè)基于 玩Android 網(wǎng)站提供的開放API實(shí)現(xiàn)的客戶端便應(yīng)運(yùn)而生了。
項(xiàng)目地址:https://github.com/LtLei/wanandroid

在1.0版本中,涵蓋了首頁(yè),分類頁(yè),搜索頁(yè),以及登錄賬號(hào)并對(duì)文章進(jìn)行收藏,查看收藏列表等功能??傮w來(lái)說(shuō)功能并不復(fù)雜,但作為演示已經(jīng)足夠了。項(xiàng)目整體采用了MVP+Lifecycle+ViewModel+LiveData,結(jié)合Room進(jìn)行數(shù)據(jù)緩存,使用Retrofit+Coroutines進(jìn)行網(wǎng)絡(luò)請(qǐng)求。為什么沒有DataBinding?請(qǐng)聽我稍后解釋。
下面是幾個(gè)我認(rèn)為在實(shí)踐中比較重要的點(diǎn),特此列出來(lái)以便大家思考以及對(duì)我進(jìn)行指點(diǎn)。
為什么是MVP而不是MVVM+DataBinding?
說(shuō)為什么不是MVVM,不如說(shuō)為什么要是MVVM。最近看到許多文章都在大力“推銷”MVVM和DataBinding,聽起來(lái)好像MVP已經(jīng)過(guò)時(shí)了,Jetpack+DataBinding已經(jīng)君臨天下,成為了Android開發(fā)的唯一范式。且不說(shuō)MVVM有沒有全面超越MVP,單就這樣的“吹捧”本身就值得我們反思。
在《也談Android應(yīng)用架構(gòu)》 中,我曾說(shuō)過(guò)MVP最大的問(wèn)題就是VP相互糾纏,即使利用了單一職責(zé)原則對(duì)P進(jìn)一步分化,采用了MVP-R技術(shù),最后因?yàn)镻要通知V的頑疾不得不留有遺憾。讓我們?cè)俅螌?duì)這個(gè)問(wèn)題進(jìn)行更深入的探究,出現(xiàn)問(wèn)題的部分如下:
public class SharedVipPresenter{
private VipRepository mVipRepository;
private SharedVipView mSharedVipView;
// ...
public void getVipInfo(){
VipInfo vi = mVipRepository.getVipInfo();
mSharedVipView.getVipInfoSuccess(vi);
}
}
public class VipPresenter{
private VipRepository mVipRepository;
private VipView mVipView;
// ...
public void getVipInfo(){
VipInfo vi = mVipRepository.getVipInfo();
mVipView.getVipInfoSuccess(vi);
}
}
可以看到為了反饋到不同的V,我們寫了幾乎一樣的代碼,且很難優(yōu)化它,于是這就成了MVP的一個(gè)顯著缺點(diǎn),也反向證明了MVVM是多么優(yōu)越。但是難以處理不代表無(wú)法處理,這時(shí)候反而可以從MVVM中吸取一點(diǎn)經(jīng)驗(yàn)了,要做的事情其實(shí)只有一件:讓P把結(jié)果通知給V。說(shuō)到通知,至少有三種方案可以考慮:
- P持有V,直接調(diào)用V的方法,也就是經(jīng)典的MVP實(shí)現(xiàn)方式
- P不再持有V,但可以通過(guò)注冊(cè)回調(diào)方式由V自行接收結(jié)果,類似Callback方式
- P不持有V,但可以使用訂閱模式,類似DataBinding
讓我們暫時(shí)拋開成見來(lái)仔細(xì)琢磨下這幾種方案的異同,你會(huì)發(fā)現(xiàn)它們只是三種不同的表現(xiàn)形式而已。不管是LiveData還是DataBinding,不都只是A通知B這一核心問(wèn)題的一種解決方式而已嗎?只不過(guò)我們使用MVP時(shí)選擇了耦合的這一種方式而已,這并不是LiveData或DataBinding本身優(yōu)秀的原因。僅在A通知B這一方面,沒有對(duì)錯(cuò),也沒有輸贏,因?yàn)榛卣{(diào)也好,訂閱也好,本就不是它們的專利。
所以,如果我們?cè)赑和V之間使用訂閱或回調(diào),也不會(huì)對(duì)DataBinding等產(chǎn)生任何“侵權(quán)”行為,但DataBinding給我們以啟示,使得我們能跳出表現(xiàn)層而看出更深層次的含義,這一點(diǎn)倒不得不給它“頒獎(jiǎng)”了。有了這一層認(rèn)知,現(xiàn)在可以非常肯定的說(shuō),Jetpack和MVVM根本就是兩個(gè)方向的東西,唯一看起來(lái)有些相似的不過(guò)是都用了訂閱模式而已,因而沒有必要綁定在一起,Jetpack和MVP一樣可以無(wú)縫銜接。
沒有了后顧之憂,我們就可以好好觀察一番MVVM了,在Android中實(shí)現(xiàn)MVVM主要靠DataBinding。這無(wú)疑是一個(gè)偉大的框架,是會(huì)讓無(wú)數(shù)人愛不釋手的框架,但如果你還沒有使用它倒也不必驚慌。DataBinding的核心是數(shù)據(jù)綁定,也就是把Model綁定到UI組件上,同時(shí)也負(fù)責(zé)Model和UI之間的數(shù)據(jù)同步問(wèn)題。在使用上的體驗(yàn)就是樣板代碼大大減少了,這應(yīng)該是最主要最明顯的感受了。
這種體驗(yàn)確實(shí)是無(wú)法拒絕的,但因“一腔熱血”而全身心投入是不夠理智的行為。經(jīng)過(guò)仔細(xì)分析,DataBinding還是有一大一小兩個(gè)問(wèn)題:
問(wèn)題1 UI復(fù)用
這應(yīng)該是再小不過(guò)的問(wèn)題了,使用DataBinding后Layout將很難復(fù)用,雖然這和UI優(yōu)化有一定的沖突,但不是所有的都不能復(fù)用,也不是所有人都會(huì)嚴(yán)格地遵守復(fù)用原則(如果沒有極強(qiáng)的規(guī)范,難道不是直接寫布局最簡(jiǎn)便?)。所以和它的優(yōu)勢(shì)相比,這個(gè)問(wèn)題的影響可以小到不計(jì)了。
問(wèn)題2 設(shè)計(jì)模式上的打擊
我認(rèn)為這是相對(duì)較大的一個(gè)問(wèn)題,DataBinding把原本僅在Activity/Fragment中的UI邏輯部分地搬到了Layout中。原本的Layout文件和Model毫不相干,是純粹的UI組件,它的數(shù)據(jù)綁定完全由Activity來(lái)操作,DataBinding把這個(gè)步驟搬到了Layout里,但并不徹底,一些操作還是需要Activity+Layout組合完成。這個(gè)現(xiàn)象破壞了一些原則,我們經(jīng)常強(qiáng)調(diào)單一職責(zé),但沒怎么提過(guò)一件事應(yīng)該有始有終,已經(jīng)不可再分的一個(gè)職責(zé)明顯沒必要由兩個(gè)組件一起完成。
所以說(shuō),如果你已經(jīng)選擇了DataBinding,那么繼續(xù)使用也沒有什么問(wèn)題,一個(gè)優(yōu)點(diǎn)和一個(gè)缺點(diǎn)互相抵消,至少表明這樣做是不會(huì)錯(cuò)的。但是如果你還沒有打算使用它,也沒必要因?yàn)闊岫榷庇谇袚Q,盡管我們相信未來(lái)MVVM一定會(huì)大放異彩,但能夠一起出道的是不是DataBinding還猶未可知,所以保持觀望也未免不是一種明智的選擇。另外,Google近期又推出了ViewBinding大殺器,很久就會(huì)到來(lái)的還有Compose,這一切不都說(shuō)明了戰(zhàn)爭(zhēng)才剛剛開始嗎?
如果你也希望直達(dá)戰(zhàn)爭(zhēng)的結(jié)果,那就和我一樣作“冷眼旁觀”狀吧,硝煙結(jié)束的一刻,才是MVP光榮退役的真正時(shí)刻。
關(guān)于依賴注入(DI)
關(guān)于DI,早就有一個(gè)家喻戶曉的框架叫做Dagger2,但它出場(chǎng)的概率和其他框架相比實(shí)在差距太遠(yuǎn)了。我想不外乎兩個(gè)原因,一是Dagger本身太復(fù)雜了,學(xué)習(xí)成本奇高,二就是DI本身并沒有那么被重視(當(dāng)然我是講在廣泛的范圍下)。DI已經(jīng)算是非常基礎(chǔ)的思想了,想來(lái)大家都很了解,這里不多作介紹了。不過(guò)不使用Dagger照樣可以做好DI,如果你被Dagger折磨過(guò),那么手動(dòng)DI一定會(huì)讓你愛不釋手的。
LiveData使用的注意事項(xiàng)
當(dāng)屏幕旋轉(zhuǎn)或頁(yè)面被回收,或其他原因?qū)е马?yè)面生命周期變化后,由于LiveData被很好地保持了下來(lái),當(dāng)頁(yè)面重建后通過(guò)新的Observer與之關(guān)聯(lián)時(shí),必定會(huì)觸發(fā)onChange方法把數(shù)據(jù)同步到頁(yè)面中(如果有數(shù)據(jù)的話),從添加Observer的流程就可以看出這一點(diǎn):
// 添加Observer的過(guò)程
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
// ...
}
// LifecycleBoundObserver實(shí)現(xiàn)了LifecycleEventObserver,當(dāng)生命周期變化時(shí)會(huì)回調(diào) onStateChanged 方法
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
// ...
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}
}
// 經(jīng)過(guò)一系列操作,當(dāng)生命周期變化時(shí)會(huì)調(diào)用dispatchingValue方法
void activeStateChanged(boolean newActive) {
// ...
if (mActive) {
dispatchingValue(this);
}
}
// 通過(guò)considerNotify決定是否需要同步數(shù)據(jù)
void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}
// 通過(guò)對(duì)比LiveData的mVersion字段和Observer的mLastVersion字段,決定是否需要同步數(shù)據(jù)
private void considerNotify(ObserverWrapper observer) {
// ...
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}
當(dāng)Observer剛添加進(jìn)來(lái)時(shí),它的mLastVersion是-1,而LiveData除非沒操作過(guò)否則一定不是-1,這時(shí)候就必然回調(diào)onChange了。
這對(duì)我們有什么影響呢?假如LiveData中存儲(chǔ)的是頁(yè)面需要的數(shù)據(jù),例如一個(gè)列表,當(dāng)頁(yè)面重建后恢復(fù)列表的數(shù)據(jù),這正是我們想要的。但假如我們進(jìn)行的是單次操作,例如點(diǎn)擊按鈕進(jìn)行收藏,LiveData用來(lái)說(shuō)明收藏結(jié)果,這時(shí)重建后LiveData的值會(huì)再次反映到頁(yè)面中,就會(huì)看到類似旋轉(zhuǎn)一次屏幕就彈出一次“收藏成功”的怪誕現(xiàn)象了。
一種方式是對(duì)于單次操作,每次使用完數(shù)據(jù)后手動(dòng)置為null,null通常會(huì)被我們過(guò)濾掉,所以即使onChange回調(diào)了也不會(huì)有問(wèn)題。不過(guò)手動(dòng)置空給我們?cè)黾恿素?fù)擔(dān),必須在想到這是一次單次操作的同時(shí)想到置空,所以最好是我們知道它是單次操作后,可以自動(dòng)處理這種情況。這里提供一種較為合理,侵入性又較小的方式,使用Event包裝返回?cái)?shù)據(jù),使用EventObserver代替Observer即可:
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): T = content
}
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
關(guān)于LiveDataBus
本項(xiàng)目中并沒有使用LiveDataBus,但還是有必要談一下關(guān)于消息總線問(wèn)題的一些想法?,F(xiàn)階段消息總線主要有EventBus,RxBus以及這個(gè)LiveDataBus三種實(shí)現(xiàn),前兩種早已在漫長(zhǎng)的時(shí)間里經(jīng)過(guò)了無(wú)數(shù)檢驗(yàn),我們主要談一談LiveDataBus。
當(dāng)一個(gè)頁(yè)面的狀態(tài)改變,需要通知到許多頁(yè)面時(shí),就需要消息總線了。不管是EventBus還是RxBus,以及現(xiàn)在的LiveDataBus,都是基于訂閱模式實(shí)現(xiàn)的,所不同的是LiveData具備了生命周期安全的優(yōu)勢(shì)。但正如上面所說(shuō)的,LiveDataBus也會(huì)有一訂閱就收到數(shù)據(jù)的問(wèn)題,而且由于這是一對(duì)多的關(guān)系,不能通過(guò)類似Event那樣的方式解決。因此當(dāng)你看到LiveDataBus時(shí),基本上都是通過(guò)反射修改了Observer的mLastVersion字段值,使之與LiveData當(dāng)前的mVersion一致,來(lái)變相達(dá)到目的。
其實(shí)我并不認(rèn)為這是一種好的解決方案,LiveData并不是為了消息總線而設(shè)計(jì)的,它的Observer也僅僅是頁(yè)面級(jí)的組件,不應(yīng)該處理跨頁(yè)面級(jí)的事務(wù),這種反射實(shí)際上給LiveData增加了不屬于它的能力,破壞了原結(jié)構(gòu)的完整性。在LiveData本身不具備這個(gè)機(jī)制前,保守地使用專業(yè)的、經(jīng)過(guò)檢驗(yàn)的EventBus和RxBus等,也是一種合情合理的態(tài)度。
雖然LiveDataBus算不得脫穎而出,LiveData本身還是可以處理一些和數(shù)據(jù)更新相關(guān)的事情的。例如很多頁(yè)面離不開User信息,那么維護(hù)一個(gè)公用的UserLiveData就可以保證任何時(shí)候取得的信息都是最新的,這也是LiveData服務(wù)相當(dāng)周到的一點(diǎn)體現(xiàn)吧。
關(guān)于Repository的小優(yōu)化
在M層的優(yōu)化中,有一步驟是使用Repository來(lái)處理全部的業(yè)務(wù),包含純粹的M和經(jīng)數(shù)據(jù)加工后的M兩部分。但是伴隨M增大,需要處理的數(shù)據(jù)也會(huì)變多,Repository也會(huì)發(fā)生體積暴漲問(wèn)題。對(duì)其進(jìn)行優(yōu)化我覺得有一個(gè)很重要的前提,那就是要M之外的組件對(duì)此零感知,ViewModel永遠(yuǎn)僅依賴Repository本身。在這個(gè)前提下可以進(jìn)行的優(yōu)化空間并不大,主要是把業(yè)務(wù)進(jìn)行分組,由一系列的小Repository分別處理一部分問(wèn)題,分組要選擇合適的粒度,太細(xì)的話就會(huì)發(fā)生類的數(shù)量暴漲了。
總結(jié)
理論總是空洞的,使人覺得似懂非懂,這次的實(shí)踐在一些方面對(duì)其進(jìn)行了很好的詮釋。當(dāng)然它不是也不會(huì)是Android開發(fā)的最佳實(shí)踐,技術(shù)會(huì)不斷進(jìn)步,思想本身也會(huì)不斷演進(jìn),而且APP還有非常多的方面需要我們注意,我們需要的是保持活躍的思維跟進(jìn)技術(shù)思想變革,同時(shí)也要保持理智,要對(duì)事物有自己的分辨。
在這次實(shí)踐里,我認(rèn)為最大的啟示是不要過(guò)于貪心,不能執(zhí)著于把每一部分都做到完美,反而是把目標(biāo)變小一些,僅在一個(gè)范圍內(nèi)做到最好,然后逐漸擴(kuò)展到全局,也就是實(shí)踐出真知,眼高手低是無(wú)法把事情做到卓越的。
后續(xù)規(guī)劃
1.0版本僅僅是開始,我們演示了架構(gòu)最基礎(chǔ)的結(jié)構(gòu),在一些類上做了一定的優(yōu)化,但APP的開發(fā)還有更多的知識(shí)需要探索。所以接下來(lái)讓我們繼續(xù)揚(yáng)帆起航,探索更多的奧秘吧。
后續(xù)我會(huì)先介紹單元測(cè)試的作用,并展示更多組件的使用方式,以及當(dāng)項(xiàng)目變大時(shí)使用組件化優(yōu)化等方面的問(wèn)題,接下來(lái)是對(duì)一些基礎(chǔ)知識(shí)的深入探索,最后會(huì)對(duì)一些面向未來(lái)的新事物略窺一二。
學(xué)無(wú)止境,希望能和熱愛學(xué)習(xí)的你,共勉。
本項(xiàng)目github地址 https://github.com/LtLei/wanandroid
我是飛機(jī)醬,如果您喜歡我的文章,可以關(guān)注我~
編程之路,道阻且長(zhǎng)。唯,路漫漫其修遠(yuǎn)兮,吾將上下而求索。