WanAndroid——探索Android應(yīng)用架構(gòu)的一次實(shí)踐

《也談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

Wan Android

在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)兮,吾將上下而求索。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容