天天看點

Model-View-Intent 模式下的響應式應用 - 第一部分 - Model(模型) 結論

<b>本文講的是Model-View-Intent 模式下的響應式應用 - 第一部分 - Model(模型),</b>

<b></b>

當我想明白原來我的模型類一直被我用錯了,一大堆以前我遇到的安卓平台相關的問題就都消失了。更重要的是,我終于使用RxJava和模型-視圖-意圖模型來建構響應式應用了,因為我從來沒成功過,盡管我已經建構過很多響應式應用,但與我馬上要在部落格上寫的這一系列不是一個等級的。在第一部分,我想講講模型以及為什麼模型這麼重要。

我說“我的模型類一直被我用錯了”是什麼意思呢?其實,有很多架構模式可以把視圖和模型分開。最受歡迎的幾種(至少在安卓開發中)是模型-視圖-控制器(MVC),模型-視圖-提供器(MVP)以及模型-視圖-視圖模型(MVVM)。通過觀察這些模式的名字,不知道你有沒有注意到什麼。它們都說到了“模型”。我發現大多數情況下我根本就沒有用到模型。

例子:從背景加載一份人員清單。一個“傳統的” MVP 實作看起來就像下面這樣:

但是,哪裡或者哪個是所謂的“模型”呢?是背景嗎?當然不是,那個是業務邏輯。那是名單嗎?也不是,那個隻是我們的視圖顯示的一部分,跟加載訓示器或錯誤資訊一樣。那麼,到底哪個是“模型”呢?

在我看來,這裡應該有個“模型”類像下面這樣:

然後,Presenter(提供器)可以像這樣實作:

現在,視圖有一個模型可以顯示在螢幕上。這并不是什麼新的概念。Trygve Reenskaug 所定義的最原始的 MVC 有相近的概念:視圖觀察模型的改變。不幸的是,MVC 這個名詞一直被誤用于描述很多不同于 Reenskaug 在 1979 年所定義的模式。例如,後端開發者使用 MVC 架構,iOS 使用 VieController。但在 Android 裡 MVC 到底意味着什麼呢?Activity 是控制器嗎?那麼 ClickListener 是什麼呢?如今,大家對于 MVC 這個名詞有很大的誤解并且被誤用,已經曲解了 Reenskaug 的原意。讓我們先不急着讨論 MVC,這可能會讓我們偏離正軌。

我們回到我最初的論點。一個“模型”解決了很多我們在安卓開發中常遇到并頭疼的問題。

狀态問題

螢幕方向的改變

傳回棧導航

程序死亡

不可變的單向資料流

可調試且可複現的狀态

易測性

讓我們一起讨論這幾點問題,看看是在 MVP 和 MVVM 模式下,這些問題是如何用“傳統”的實作來解決,最後“Model(模型)”是怎麼幫助我們繞過通常的陷阱。

響應式應用 - 真是一個時髦的詞語。我用這個詞來表達一個應用通過 UI 來響應狀态的改變。啊,對,我們有另外一個詞:“State(狀态)”。什麼是 “State” 呢?其實,通常我們把 “State” 描述為我們在螢幕上的所見,比如視圖顯示進度條時表示“正在加載的狀态”。關鍵在于:我們的前端開發者趨于關注 UI。這并不一定是一件壞事因為最終使用者是否會使用我們的應用是由 UI 的好壞決定,而這也決定了一個應用的成敗。但是,看一下上面簡單的 MVP 例子(沒有用到 PersonsModel 的那個)。在這裡,UI 的狀态是與 Presenter(提供器)協作的,因為顯示器會告訴視圖需要顯示什麼。同樣地,這也适用于 MVVM。在這篇部落格裡,我想要差別兩種 MVVM 實作:第一種使用安卓的資料綁定,第二種使用 RxJava。在使用資料綁定的 MVVM 模式中,狀态直接放在 ViewModel(視圖模型)中:

在使用 RxJava 的 MVVM 模式中,我們不使用資料綁定引擎,但是我們綁定 Observable 對象到視圖裡的 UI 部件上。

當然,這些代碼片段并不完美。你的實作也可能看起來完全不同。然而重點在于,在 MVP 和 MVVM 模式下,狀态是由 Presenter(提供器)或是 ViewModel(視圖模型)驅動。

我們由此可以發現:

業務邏輯有自己的狀态,Presenter(提供器)(或是 ViewModel(視圖模型))有自己的狀态(并且你嘗試同步業務邏輯和 Presenter(提供器)兩者間的狀态,使它們一緻),并且 View(視圖)可能也有自己的狀态(比如,你直接在視圖中設定了可見性,或者安卓本身在重建時從綁定中恢複狀态)。

Presenter(提供器)(或是 ViewModel(視圖模型))有很多任意的輸入(View (視圖)觸發一個動作,由 Presenter(提供器)處理)是可以的,但同時,Presenter(提供器)也有很多輸出(或者說在 MVP 中 view.showLoading() 或 view.showError() 的輸出方式,以及 ViewModel(視圖模型)提供的多種 Observable 對象)。這就會導緻 當View(視圖),Presenter(提供器)和業務邏輯三者在不同線程工作時的狀态沖突。

Model-View-Intent 模式下的響應式應用 - 第一部分 - Model(模型) 結論

最好的情況是,狀态沖突隻導緻了一些可見的問題,例如同時顯示了加載訓示器(“加載狀态”)和錯誤訓示器(“錯誤狀态”):

而在最壞的情況下,你會收到像 Crashlytics 這樣的崩潰回報工具的嚴重缺陷報告。并且你将無法重制它而很可能無法修複。

但假如我們有且僅有一個,由下(業務邏輯餓)至上(視圖)的可信狀态來源呢?實際上,在這篇部落格開頭提到“Model(模型)”的時候,我們就已經看到了一個相似的概念。

猜猜怎麼着?模型反應狀态。一旦我們了解了這個,我們就可以解決(同時也可以從源頭上避免)很多和狀态相關的問題,并且,我的 Presenter(提供器)隻有一個輸出:getView().render(PersonsModel)。這個反應了一個簡單的數學函數, f(x) = y(也可能是由多個輸入的函數的如 f(a,b,c),但隻有一個輸出)。可能不是每個人都喜歡數學,但是數學家不知道這樣的代碼缺陷在哪裡,而軟體工程師知道。

了解“Model(模型)”并知道如何正确實作它是非常關鍵的,因為 Model(模型)最後可以解決“狀态問題”。

在安卓中,螢幕方向改變是一個具有挑戰性的問題。最簡單的方法是忽略它,在每次螢幕方向改變的時候重新加載所有東西。這絕對是一個有效的解決方法。大多數情況下,你的應用也離線工作,是以資料從本地資料庫或者其他本地緩存而來。是以,當螢幕方向改變後,加載資料非常快。但是,我個人不喜歡看到加載訓示器,盡管它隻顯示幾毫秒,因為我認為這不是流暢的使用者體驗。是以人們(包括我自己)開始使用保留提供器(Presenter)的MVP。即使視圖(View)在螢幕方向改變時被分離(和銷毀),然而提供器(Presenter)還在記憶體中,視圖(View)可以重新被附着到Presenter上。

同樣的概念适用于使用 RxJava 的 MVVM 模式,但是我們要記住,一旦 View(視圖)取消訂閱 ViewModel(視圖模型),可觀察的流就會被銷毀。你可以對這個主題做些功課作為例子。在使用資料綁定的 MVVM 模式中,ViewModel(視圖模型)是通過資料綁定引擎直接綁定在 View(視圖)上的。為了避免記憶體洩漏,我們必須在螢幕方向變化時銷毀 ViewModel(視圖模型)。

Model-View-Intent 模式下的響應式應用 - 第一部分 - Model(模型) 結論

我非常确定有其它的方法可以解決視圖的狀态問題。我們退一步總結那些庫想要解決的問題:他們想要解決我們已經讨論過的狀态問題。

是以,再一次,用一個“Model(模型)”來反映目前的“狀态”,并且用一個方法來“顯示” “Model(模型)”解決了這個問題,這跟調用getView().render(PersonsModel) 一樣簡單(當把 View(視圖)重新附在 Presenter(提供器)上時使用最近一次的模型)。

當視圖不再被使用以後,Presenter(提供器)(或 ViewModel(視圖模型))需要被保留嗎?舉個例子,如果 Fragment(視圖) 已經被另外一個 Fragment 替換因為使用者已經導航到别的螢幕上了,那麼 Presenter(提供器)就沒有視圖附着在上面了。如果沒有視圖附着,顯然地,Presenter(提供器)并不能更新來自業務邏輯最新的資料。但如果使用者回來會怎麼樣呢?(即按下傳回按鈕彈出傳回棧)重新加載資料或者重用目前的Presenter(提供器)?這更像是個哲學問題。通常來說,一旦使用者傳回到前一個螢幕(彈出傳回棧),他希望從他離開的地方繼續操作。這就是在2中讨論的“恢複視圖狀态問題”。是以,這個解決方案非常直截了當:使用一個 “Model(模型)”來表示狀态,當我們從傳回棧回來時,我們隻要調用getView().render(PersonsModel) 展示視圖。

我覺得這是一個安卓開發中的誤區:程序死亡是一件壞事情,而且在程序死亡後我們需要庫來幫助我們恢複狀态(還有 Presenters(提供器) 或者 ViewModels(視圖模型))。首先,程序死亡的原因隻有一個:安卓運作系統需要提供更多的資源給别的應用或為了節省電量。但是當你的應用在前台運作并且被應用使用者使用,這将不會發生。是以,不要試圖與平台的規則抗争了。如果你真的有一些需要長時間在背景運作的工作,用 Service。因為它是唯一的途徑,告訴運作系統你的應用還需要“使用”。

如果程序死亡發生了,安卓會提供一些回調類似 onSaveInstanceState() 儲存狀态。這裡又提到狀态了。我們應不應該把視圖資訊儲存到 Bundle 裡?同樣,Presenter(提供器)有沒有它自己的狀态需要我們儲存到 Bundle 裡的呢?那業務邏輯狀态呢?我們已經了解到:正如在第1,第2和第3點中形容的,我們隻需要一個模型累來反映整個狀态。這樣的話,儲存模型到 Bundle 裡以及之後恢複它就會很簡單了。

但是,我個人認為,大多數時候,重載整個螢幕就像打開第一個應用一樣比儲存狀态更好。試想,NewsReader 應用展示了新聞清單。當我們的應用被殺掉而我們儲存了狀态。6個小時後使用者重新打開我們的應用,狀态恢複,我們的應用可能顯示的是過時的内容。在這種情境下,不儲存模型/狀态,而隻是重新加載資料也許更合适。

Model-View-Intent 模式下的響應式應用 - 第一部分 - Model(模型) 結論

這樣我們就建立起了一個單向資料流,而業務邏輯就是這個單一可信源。它建立了不可變的模型執行個體。但是這看起來對一個簡單的計數器過于複雜了。是的,計數器不過是一個簡單的應用。大部分的應用從簡單開始,但是很快就會變得複雜。我認為單一的資料流河不可變的模型是非常必要的,甚至是對一個簡單的應用。它能保證應用在複雜度增長時維持簡單(從開發者的角度看)。

另外,單一的資料流讓我們的應用易于調試。下一次,當我們從 Crashlytics 拿到一個崩潰日志,我們能簡單複現并修複問題,因為所有需要的資訊都附在崩潰日志上,這不是很棒嗎?當然,我們所需要的所有資訊是目前的模型和使用者造成崩潰的行為(也就是單擊減少按鈕)。這些就是我們複現崩潰所需要的所有東西。而且,那些資訊非常容易記錄到崩潰日志中。如果不是單一資料流(比如,有人誤用 EventBugs 并且把 CounterModels 傳得到處都是)和不可變性(我們将沒辦法确定到底是誰修改了模型),就不會那麼簡單。

“傳統的” MVP 或者 MVVM 改進了應用的易測性。MVC 也是可測的:從來沒有人說過我們需要把所有的業務邏輯放進 activity。通過用模型反映狀态,我們可以簡化單元測試代碼,因為隻需要檢查 assertEquals(expectedModel, model)。這消除了很多我們本來需要模拟的對象。另外,這樣就移除了很多調用 Mockito.verify(view, times(1)).showFoo() 方法的驗證測試。最後,測試代碼的可讀性會更高,也更容易了解和維護,因為我們不需要處理真實代碼裡的實作細節。

這一系列部落格的第一部分我們讨論了很多理論的東西。我們真的需要一個部落格專門說模型嗎?我覺得了解模型是非常重要和基本的,它能幫助我們避免難對付的問題。模型并不等于業務邏輯。而是業務邏輯(也就是應用裡的用例)生産模型。在第二部分,當我們最後建構好 Model-View-Intent(模型-視圖-意圖模型)模式的響應式應用後,我們可以看到理論上的模型是怎麼實作的。我們馬上要建構的示例應用是一個虛拟線上商店。關于你在第二部分想知道的東西,這裡有一個短的預告片。敬請期待。

Model-View-Intent 模式下的響應式應用 - 第一部分 - Model(模型) 結論

<b>原文釋出時間為:2017年2月8日</b>

<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>

繼續閱讀