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

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