[譯]使用MVI打造響應(yīng)式APP(一):Model到底是什么

原文:《REACTIVE APPS WITH MODEL-VIEW-INTENT - PART1 - MODEL》
作者:Hannes Dorfmann
譯者:卻把清梅嗅

有朝一日,我突然發(fā)現(xiàn)我對(duì)于Model層的定義 全部是錯(cuò)誤的逛绵,更新了認(rèn)知后塌鸯,我發(fā)現(xiàn)曾經(jīng)我在Android平臺(tái)上主題討論中的那些困惑或者頭痛都消失了卧斟。

從結(jié)果上來(lái)說(shuō),最終我選擇使用 RxJavaModel-View-Intent(MVI) 構(gòu)建 響應(yīng)式的APP牡辽,這是我從未有過(guò)的嘗試——盡管在這之前我開(kāi)發(fā)的APP也是響應(yīng)式的俯渤,但 響應(yīng)式編程 的體現(xiàn)與這次實(shí)踐相比,完全無(wú)法相提并論棒坏,在接下來(lái)我將要講述的一系列文章中,你也會(huì)感受到這些遭笋。但作為系列文章的開(kāi)始坝冕,我想先闡述一個(gè)觀點(diǎn):

所謂的Model層到底是什么,我之前對(duì)Model層的定義出現(xiàn)了什么問(wèn)題瓦呼?

我為什么說(shuō) 我對(duì)Model層有著錯(cuò)誤的理解和使用方式 呢喂窟?當(dāng)然测暗,現(xiàn)在有很多架構(gòu)模式將View層和Model層進(jìn)行了分離,至少在Android開(kāi)發(fā)的領(lǐng)域磨澡,最著名的當(dāng)屬Model-View-Controller (MVC)碗啄、Model-View-Presenter (MVP)Model-View-ViewModel (MVVM)——你注意到了嗎?這些架構(gòu)模式中稳摄,Model都是不可或缺的一環(huán)稚字,但我意識(shí)到 在絕大數(shù)情況下,我根本沒(méi)有Model厦酬。

舉例來(lái)說(shuō)胆描,一個(gè)簡(jiǎn)單的從后端拉取Person列表情況下,傳統(tǒng)的MVP實(shí)現(xiàn)方式應(yīng)該是這樣的:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().showLoading(true); // 展示一個(gè) ProgressBar

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().showPersons(persons); // 展示用戶列表
      }

      public void onError(Throwable error){
        getView().showError(error); // 展示錯(cuò)誤信息
      }
    });
  }
}

但是仗阅,這段代碼中的Model到底是指什么呢昌讲?是指后臺(tái)的網(wǎng)絡(luò)請(qǐng)求嗎?不减噪,那只是業(yè)務(wù)邏輯短绸。是指請(qǐng)求結(jié)果的用戶列表嗎?不筹裕,它和ProgressBar醋闭、錯(cuò)誤信息的展示一樣,僅僅只代表了View層所能展示內(nèi)容的一小部分而已饶碘。

那么,Model層究竟是指什么呢馒吴?

從我個(gè)人理解來(lái)說(shuō),Model類(lèi)應(yīng)該定義成這樣:

class PersonsModel {
  // 在真實(shí)的項(xiàng)目中扎运,需要定義為私有的
  // 并且我們需要通過(guò)getter和setter來(lái)訪問(wèn)它們
  final boolean loading;
  final List<Person> persons;
  final Throwable error;

  public(boolean loading, List<Person> persons, Throwable error){
    this.loading = loading;
    this.persons = persons;
    this.error = error;
  }
}

這樣的實(shí)現(xiàn),Presenter層應(yīng)該這樣實(shí)現(xiàn):

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().render( new PersonsModel(true, null, null) ); // 展示一個(gè) ProgressBar

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().render( new PersonsModel(false, persons, null) );  // 展示用戶列表
      }

      public void onError(Throwable error){
          getView().render( new PersonsModel(false, null, error) ); // 展示錯(cuò)誤信息
      }
    });
  }
}

現(xiàn)在饮戳,View層持有了一個(gè)Model豪治,并且能夠借助它對(duì)屏幕上的控件進(jìn)行rendered(渲染)。這并非什么新鮮的概念扯罐,Trygve Reenskaug在1979年時(shí)负拟,其對(duì)最初版本的MVC定義中具有相似的概念:View觀察Model的變化

然而歹河,MVC這個(gè)術(shù)語(yǔ)被用來(lái)描述太多種不同的模式掩浙,這些模式與Reenskaug在1979年制定的模式并不完全相同。比如后端開(kāi)發(fā)人員使用MVC框架秸歧,iOS有ViewController厨姚,到了Android領(lǐng)域MVC又被如何定義了呢?ActivityController嗎? 那這樣的話ClickListener又算什么呢键菱?如今谬墙,MVC這個(gè)術(shù)語(yǔ)變成了一個(gè)很大的誤區(qū),它錯(cuò)誤地理解和使用了Reenskaug最初制定的內(nèi)容——這個(gè)話題到此為止,再繼續(xù)下去整個(gè)文章就會(huì)失控了拭抬。

言歸正傳部默,Model的持有將會(huì)解決許多我們?cè)?code>Android開(kāi)發(fā)中經(jīng)常遇到的問(wèn)題:

  • 1.狀態(tài)問(wèn)題
  • 2.屏幕方向的改變
  • 3.在頁(yè)面堆棧中導(dǎo)航
  • 4.進(jìn)程終止
  • 5.單向數(shù)據(jù)流的不變性
  • 6.可調(diào)試和可重現(xiàn)的狀態(tài)
  • 7.可測(cè)試性

要討論這些關(guān)鍵的問(wèn)題,我們先來(lái)看看“傳統(tǒng)”的MVPMVVM的實(shí)現(xiàn)代碼中如何處理它們造虎,然后再談Model如何跳過(guò)這些常見(jiàn)的陷阱傅蹂。

1.狀態(tài)問(wèn)題

響應(yīng)式App,這是最近非常流行的話題累奈,不是嗎贬派?所謂的 響應(yīng)式App 就是 應(yīng)用會(huì)根據(jù)狀態(tài)的改變作出UI的響應(yīng),這句話里有一個(gè)非常好的單詞:狀態(tài)澎媒。什么是狀態(tài)呢搞乏?大多數(shù)時(shí)間里,我們將 狀態(tài) 描述為我們?cè)谄聊恢锌吹降臇|西戒努,例如當(dāng)界面展示ProgressBar時(shí)的loading state请敦。

很關(guān)鍵的一點(diǎn)是,我們前端開(kāi)發(fā)人員傾向?qū)W⒂赨I储玫。這不一定是壞事侍筛,因?yàn)橐粋€(gè)好的UI體驗(yàn)決定了用戶是否會(huì)用你的產(chǎn)品,從而決定了產(chǎn)品能否獲得成功撒穷。但是看看上述的MVP示例代碼(不是使用了PersonModel的那個(gè)例子)匣椰,這里UI的狀態(tài)由Presenter進(jìn)行協(xié)調(diào),Presenter負(fù)責(zé)告訴View層如何進(jìn)行展示端礼。

MVVM亦然禽笑,我想在本文中對(duì)MVVM的兩種實(shí)現(xiàn)方式進(jìn)行區(qū)分:第一種依賴(lài)DataBinding庫(kù),第二種則依賴(lài)RxJava蛤奥;對(duì)于依賴(lài)DataBinding的前者佳镜,其狀態(tài)被直接定義于ViewModel中:

class PersonsViewModel {
  ObservableBoolean loading;
  // 省略...

  public void load(){

    loading.set(true);

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
      loading.set(false);
      // 省略其它代碼,比如對(duì)persons進(jìn)行渲染
      }

      public void onError(Throwable error){
        loading.set(false);
        // 省略其它代碼凡桥,比如展示錯(cuò)誤信息
      }
    });
  }
}

使用RxJava實(shí)現(xiàn)MVVM的方式中蟀伸,其并不依賴(lài)DataBinding引擎,而是將Observable和UI的控件進(jìn)行綁定缅刽,例如:

class RxPersonsViewModel {
  private PublishSubject<Boolean> loading;
  private PublishSubject<List<Person> persons;
  private PublishSubject loadPersonsCommand;

  public RxPersonsViewModel(){
    loadPersonsCommand.flatMap(ignored -> backend.loadPersons())
      .doOnSubscribe(ignored -> loading.onNext(true))
      .doOnTerminate(ignored -> loading.onNext(false))
      .subscribe(persons)
      // 實(shí)現(xiàn)方式并不惟一
  }

  // 在View層訂閱它 (比如 Activity / Fragment)
  public Observable<Boolean> loading(){
    return loading;
  }

  // 在View層訂閱它 (比如 Activity / Fragment)
  public Observable<List<Person>> persons(){
    return persons;
  }

  // 每當(dāng)觸發(fā)此操作 (即調(diào)用 onNext()) 啊掏,加載Persons數(shù)據(jù)
  public PublishSubject loadPersonsCommand(){
    return loadPersonsCommand;
  }
}

當(dāng)然,這些代碼并非完美衰猛,您的實(shí)現(xiàn)方式可能截然不同脖律;我想說(shuō)明的是,通常在MVP或者MVVM中腕侄,狀態(tài) 是由ViewModel或者Presenter進(jìn)行驅(qū)動(dòng)的小泉。

這導(dǎo)致下述情況的發(fā)生:

  • 1.業(yè)務(wù)邏輯本身也擁有了狀態(tài)芦疏,Presenter(或者ViewModel)本身也擁有了狀態(tài)(并且,你還需要通過(guò)代碼去同步它們的狀態(tài)使其保持一致)微姊,同時(shí)酸茴,View可能也有自己的狀態(tài)(比方說(shuō),調(diào)用ViewsetVisibility()方法設(shè)置其可見(jiàn)性兢交,或者Android系統(tǒng)在重新創(chuàng)建時(shí)從bundle恢復(fù)狀態(tài))薪捍。

  • 2.Presenter(或ViewModel)有任意多個(gè)輸入(View層觸發(fā)行為并交給Presenter處理),這是ok的配喳,但同時(shí)Presenter也有很多輸出(或MVP中的輸出通道酪穿,如view.showLoading()view.showError();在MVVM中,ViewModel的實(shí)現(xiàn)中也提供了多個(gè)Observable晴裹,這最終導(dǎo)致了View層被济,Presenter層和業(yè)務(wù)邏輯中狀態(tài)的沖突,在處理多線程的時(shí)候涧团,這種情況更明顯只磷。

在好的情況下,這只會(huì)導(dǎo)致視覺(jué)上的錯(cuò)誤泌绣,例如同時(shí)顯示加載指示符(“加載狀態(tài)”)和錯(cuò)誤指示符(“錯(cuò)誤狀態(tài)”)钮追,如下所示:

在最糟糕的情況下,您從崩潰報(bào)告工具(如Crashlytics)接收到了一個(gè)嚴(yán)重的錯(cuò)誤報(bào)告阿迈,但您無(wú)法重現(xiàn)這個(gè)錯(cuò)誤元媚,因此也幾乎無(wú)從著手去修復(fù)它。

如果從 底層 (業(yè)務(wù)邏輯層)到 頂層 (UI視圖層)苗沧,有且僅有一個(gè)真實(shí)描述狀態(tài)的源刊棕,會(huì)怎么樣呢?事實(shí)上崎页,我們已經(jīng)在文章的開(kāi)頭談?wù)?code>Model的時(shí)候鞠绰,就已經(jīng)通過(guò)案例腰埂,把相似的概念展示了出來(lái):

class PersonsModel {
  final boolean loading;
  final List<Person> persons;
  final Throwable error;

  public(boolean loading, List<Person> persons, Throwable error){
    this.loading = loading;
    this.persons = persons;
    this.error = error;
  }
}

你猜怎么了飒焦? Model映射了狀態(tài),當(dāng)我想通了這點(diǎn)屿笼,許多狀態(tài)相關(guān)的問(wèn)題迎刃而解(甚至在編碼之前就已經(jīng)被避免了)牺荠;現(xiàn)在Presenter層變得只有一個(gè)輸出了:

getView().render(PersonsModel)

它對(duì)應(yīng)了一個(gè)數(shù)學(xué)上簡(jiǎn)單的函數(shù),比如f(x) = y,對(duì)于多個(gè)輸入的函數(shù)驴一,對(duì)應(yīng)的則是f(a,b,c),但也是一個(gè)輸出休雌。

并非對(duì)所有人來(lái)說(shuō)數(shù)學(xué)都是香茗,就好像數(shù)學(xué)家并不清楚bug是什么——但軟件工程師需要去品嘗它肝断。

了解Model到底是什么以及如何建立對(duì)應(yīng)的Model非常重要杈曲,因?yàn)樽罱KModel可以解決 狀態(tài)問(wèn)題驰凛。

2.屏幕方向的改變

譯者注:針對(duì) 屏幕旋轉(zhuǎn)后的狀態(tài)回溯 這個(gè)問(wèn)題,已經(jīng)可以通過(guò)Google官方發(fā)布的ViewModel組件進(jìn)行處理担扑,開(kāi)發(fā)者不再需要為此煩惱恰响,但本章節(jié)仍值得一讀。

Android設(shè)備上的 屏幕旋轉(zhuǎn) 是一個(gè)有足夠挑戰(zhàn)性的問(wèn)題;忽視它是一個(gè)最簡(jiǎn)單的解決方案涌献,即 每次屏幕旋轉(zhuǎn)胚宦,都對(duì)數(shù)據(jù)重新進(jìn)行加載 。這確實(shí)行之有效燕垃,大多數(shù)情況下枢劝,您的APP也在離線狀態(tài)下工作,其數(shù)據(jù)來(lái)源于數(shù)據(jù)庫(kù)或者其它本地緩存卜壕,這意味著屏幕旋轉(zhuǎn)后的數(shù)據(jù)加載速度是很快的您旁。

但是,個(gè)人而言我不喜歡看到加載框印叁,哪怕加載速度是毫秒級(jí)別的被冒,因?yàn)槲艺J(rèn)為這并非完美的用戶體驗(yàn),因此大家(包括我)開(kāi)始使用MVP轮蜕,這其中包括了 保留性的Presenter——這樣就可以 在屏幕旋轉(zhuǎn)時(shí)分離和銷(xiāo)毀View層昨悼,而Presenter則會(huì)保存在內(nèi)存中不會(huì)被銷(xiāo)毀,然后View層會(huì)再次連接到Presenter跃洛。

使用RxJavaMVVM也可以實(shí)現(xiàn)相同的概念率触,但請(qǐng)牢記,一旦View對(duì)ViewModel取消了訂閱汇竭,可觀察的流就會(huì)被銷(xiāo)毀葱蝗,這個(gè)問(wèn)題你可以用Subject解決;對(duì)于DataBinding構(gòu)建的MVVM來(lái)講细燎,ViewModelDataBinding直接綁定到View層两曼,為了避免內(nèi)存泄露,需要我們?cè)谄聊恍D(zhuǎn)時(shí)及時(shí)銷(xiāo)毀ViewModel玻驻。

對(duì)于 保留性的Presenter 或者 ViewModel 的問(wèn)題是: 我們?nèi)绾螌?code>View的狀態(tài)在屏幕旋轉(zhuǎn)之后回溯悼凑,保證ViewPresenter再次回到之前相同的狀態(tài)?我編寫(xiě)了一個(gè)名為 Mosby 的MVP庫(kù)璧瞬,其包含一個(gè)名為ViewState的功能户辫,它基本上將業(yè)務(wù)邏輯的狀態(tài)與View同步。 Moxy,另一個(gè)MVP庫(kù)嗤锉,提出了一個(gè)非常有趣的解決方案——通過(guò)使用commands在屏幕方向更改后重現(xiàn)View的狀態(tài):

針對(duì)View層狀態(tài)的問(wèn)題,我很確定還有其他的解決方案渔欢。讓我們退后一步,歸納一下這些庫(kù)試圖解決的問(wèn)題:那就是我們已經(jīng)討論過(guò)的 狀態(tài)問(wèn)題瘟忱。

再次重申奥额,我們通過(guò)一個(gè) 能反映當(dāng)前狀態(tài)的Model 和一個(gè)渲染Model的方法 解決了這個(gè)問(wèn)題苫幢,就像調(diào)用getView().render(PersonsModel)一樣簡(jiǎn)單。

3.在頁(yè)面堆棧中導(dǎo)航

當(dāng)View不再使用時(shí)垫挨,是否還有保留Presenter(或ViewModel)的必要态坦?比如,用戶跳轉(zhuǎn)到了另外一個(gè)界面棒拂,這導(dǎo)致Fragment(View)被另外的Fragmentreplace了伞梯,因此Presenter已經(jīng)不在被任何View持有。

如果沒(méi)有View層和Presenter進(jìn)行關(guān)聯(lián)帚屉,Presenter自然也無(wú)法根據(jù)業(yè)務(wù)邏輯谜诫,將最新的數(shù)據(jù)反映在View上。但如果用戶又回來(lái)了怎么辦(比如按下后退按鈕)攻旦,是 重新加載數(shù)據(jù) 還是 重用現(xiàn)有的Presenter?——這看起來(lái)像是一個(gè)哲學(xué)問(wèn)題喻旷。

通常用戶一旦回到之前的界面尖殃,他會(huì)期望回到之前的界面繼續(xù)操作耘成。這仍然像是第二小節(jié)關(guān)于View狀態(tài)恢復(fù) 的問(wèn)題,解決方案簡(jiǎn)明扼要:當(dāng)用戶返回時(shí)给僵,我們得到 代表狀態(tài)的Model 烙无,然后只需調(diào)用 getView().render(PersonsModel) 對(duì)View層進(jìn)行渲染锋谐。

4.進(jìn)程終止

進(jìn)程終止是一件壞事,并且我們需要依賴(lài)一些庫(kù)以幫助我們?cè)谶M(jìn)程終止后對(duì)狀態(tài)進(jìn)行恢復(fù)——我認(rèn)為這是Android開(kāi)發(fā)中常見(jiàn)的一種誤解截酷。

首先涮拗,進(jìn)程終止的原因只有一個(gè),并且有足夠充分的理由——Android操作系統(tǒng)需要更多資源用于其他應(yīng)用程序或節(jié)省電池迂苛。如果你的APP處于前臺(tái)并且正在被用戶主動(dòng)使用時(shí)三热,這種情況永遠(yuǎn)不會(huì)發(fā)生,因此三幻,遵紀(jì)守法就漾,不要與平臺(tái)作斗爭(zhēng)了(就是不要執(zhí)拗于所謂的進(jìn)程保活了)念搬。如果你真的需要在后臺(tái)進(jìn)行一些長(zhǎng)時(shí)間的工作抑堡,請(qǐng)使用Service,這也是向操作系統(tǒng)發(fā)出信號(hào),告知您的App仍處于“主動(dòng)使用狀態(tài)”的 唯一方式 锁蠕。

如果進(jìn)程終止了夷野,Android會(huì)提供一些回調(diào)以供 保存狀態(tài)懊蒸,比如onSaveInstanceState()——沒(méi)錯(cuò)荣倾,又是 狀態(tài) 。我們應(yīng)該將View的信息保存在Bundle中嗎骑丸?我們是否也應(yīng)該把Presenter中的狀態(tài)保存到Bundle中舌仍?那么業(yè)務(wù)邏輯的狀態(tài)呢妒貌?又是老生常談的問(wèn)題,就和上面三個(gè)小節(jié)談到的一樣铸豁。

我們只需要一個(gè)代表整個(gè)狀態(tài)的Model類(lèi)灌曙,我們很容易將Model保存在Bundle中并在之后對(duì)它進(jìn)行恢復(fù)。但是节芥,我個(gè)人認(rèn)為大部分情況下最好不保存狀態(tài)在刺,而是 重新加載整個(gè)界面,就像我們第一次啟動(dòng)App一樣头镊。 想想顯示新聞列表的 NewsReader App蚣驼。 當(dāng)App被殺掉,我們保存了狀態(tài)相艇,6小時(shí)后用戶重新打開(kāi)App并恢復(fù)了狀態(tài)颖杏,我們的App可能會(huì)顯示過(guò)時(shí)的內(nèi)容。因此,這種情況下坛芽,也許不存儲(chǔ)Model和狀態(tài)留储、而對(duì)數(shù)據(jù)重新加載才是更好的策略。

5.單向數(shù)據(jù)流的不變性

在這里我不打算討論不變性(immutabiliy)的優(yōu)勢(shì)咙轩,因?yàn)橛泻芏噘Y源討論這個(gè)問(wèn)題获讳。我們想要一個(gè)不可變的Model(代表狀態(tài))。為什么活喊?因?yàn)槲覀兿胍ㄒ坏臓顟B(tài)源赔嚎,在傳遞Model時(shí),我們不希望App中的其他組件可以改變我們的Model或者State胧弛。

讓我們假設(shè)編寫(xiě)一個(gè)簡(jiǎn)單的計(jì)數(shù)器App尤误,它具有遞增和遞減的功能按鈕,并在TextView中顯示當(dāng)前計(jì)數(shù)器值结缚。 如果我們的Model(在這種情況下只是計(jì)數(shù)器值损晤,即一個(gè)整數(shù))是不可變的,那么我們?nèi)绾胃挠?jì)數(shù)器红竭?

我很高興被問(wèn)到這個(gè)問(wèn)題尤勋,按鈕被點(diǎn)擊時(shí),我們并非直接操作TextView茵宪。我的建議是:

  • 1.我們的View層應(yīng)該有一個(gè)類(lèi)似view.render(...)的方法最冰;
  • 2.我們的Model是不可變的,因此不可直接修改Model;
  • 3.View的渲染有且只有一個(gè)來(lái)源:即業(yè)務(wù)邏輯稀火。

我們將點(diǎn)擊事件 下沉 到業(yè)務(wù)邏輯層暖哨。業(yè)務(wù)邏輯知道當(dāng)前的Model(例如,持有一個(gè)私有的成員Model凰狞,它代表著當(dāng)前的狀態(tài)), 這之后根據(jù)舊的Model篇裁,創(chuàng)建一個(gè)新的帶有增量/減量值的Model沛慢。

image

這樣我們建立了一個(gè) 單向數(shù)據(jù)流,業(yè)務(wù)邏輯作為單一源用于創(chuàng)建不可變的Model實(shí)例达布,但對(duì)于一個(gè)計(jì)數(shù)器來(lái)講未免有點(diǎn)小題大做团甲,不是嗎?誠(chéng)然黍聂,是的躺苦,計(jì)數(shù)器只是一個(gè)簡(jiǎn)單的應(yīng)用程序。大多數(shù)應(yīng)用程序都是以簡(jiǎn)單的應(yīng)用程序開(kāi)始产还,但復(fù)雜性增長(zhǎng)很快——從我的角度來(lái)看圾另,單向數(shù)據(jù)流和不可變模型是必要的,這會(huì)使簡(jiǎn)單的應(yīng)用程序雕沉,在復(fù)雜性遞增的同時(shí)集乔,依然保持著簡(jiǎn)單(對(duì)開(kāi)發(fā)者而言)。

6.可調(diào)試和可重現(xiàn)的狀態(tài)

此外坡椒,單向數(shù)據(jù)流保證了我們的應(yīng)用程序易于調(diào)試扰路。下次我們從Crashlytics獲得崩潰報(bào)告時(shí),我們可以輕松地重現(xiàn)并修復(fù)此崩潰倔叼,因?yàn)樗斜匦璧男畔⒍家迅郊拥奖罎?bào)告中了汗唱。

什么叫做必需的信息?那就是當(dāng)前的Model和用戶用戶在崩潰發(fā)生時(shí)想要執(zhí)行的操作(比如丈攒,點(diǎn)擊減量按鈕)哩罪。這就是我們重現(xiàn)這次崩潰所需的全部信息,這些信息非常容易收集并附加在崩潰報(bào)告中巡验。

如果沒(méi)有單項(xiàng)數(shù)據(jù)流(比如际插,對(duì)EventBus的濫用,或者將CounterModels的私有域暴露出來(lái))显设,或者沒(méi)有不變性(這會(huì)導(dǎo)致我們不知道誰(shuí)實(shí)際更改了Model)框弛,那么bug的復(fù)現(xiàn)就沒(méi)那么容易了。

7.可測(cè)試性

“傳統(tǒng)”的MVPMVVM提高了應(yīng)用程序的可測(cè)試性捕捂。MVC也是可測(cè)試的:沒(méi)有人說(shuō)我們必須將所有業(yè)務(wù)邏輯放入Activity中瑟枫。使用表示狀態(tài)的Model,我們可以簡(jiǎn)化單元測(cè)試的代碼慷妙,因?yàn)槲覀兛梢院?jiǎn)單地檢查assertEqual(expectedModel,model)膝擂。這使我們避免了許多必須要Mock的對(duì)象。

此外猿挚,這也減少了很多驗(yàn)證的測(cè)試驶鹉,即某些方法是否被調(diào)用(比如Mockito.verify(view, times(1)).showFoo()),最終室埋,這使得我們的單元測(cè)試代碼更具可讀性,易于理解并且易于維護(hù)姚淆,因?yàn)槲覀儾槐靥幚砗芏鄬?shí)際代碼的實(shí)現(xiàn)細(xì)節(jié)。

總結(jié)

在這個(gè)博客文章系列的第一部分中腌逢,我們談了很多關(guān)于理論的東西。我們真的需要關(guān)于專(zhuān)門(mén)討論Model的博客嗎搏讶?

我認(rèn)為初步地理解Model的確很重要,這也有助于我們避免一些會(huì)遇到的問(wèn)題媒惕。Model并不意味著業(yè)務(wù)邏輯,它是生成Model的業(yè)務(wù)邏輯(比如妒蔚,一次交互,一個(gè)用例肴盏,一個(gè)倉(cāng)庫(kù)或者你在APP中調(diào)用的任何東西)科盛。

在接下來(lái)的第二部分中,當(dāng)我們最終使用Model-View-Intent構(gòu)建一個(gè)響應(yīng)式App 時(shí)菜皂,我們將看到Model的實(shí)際應(yīng)用土涝。演示的APP是一個(gè)虛構(gòu)的在線商店的應(yīng)用程序,敬請(qǐng)關(guān)注幌墓。


系列目錄

《使用MVI打造響應(yīng)式APP》原文

《使用MVI打造響應(yīng)式APP》譯文

《使用MVI打造響應(yīng)式APP》實(shí)戰(zhàn)


關(guān)于我

Hello但壮,我是卻把清梅嗅,如果您覺(jué)得文章對(duì)您有價(jià)值常侣,歡迎 ??蜡饵,也歡迎關(guān)注我的博客或者Github

如果您覺(jué)得文章還差了那么點(diǎn)東西胳施,也請(qǐng)通過(guò)關(guān)注督促我寫(xiě)出更好的文章——萬(wàn)一哪天我進(jìn)步了呢溯祸?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子焦辅,更是在濱河造成了極大的恐慌博杖,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件筷登,死亡現(xiàn)場(chǎng)離奇詭異剃根,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)前方,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)狈醉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人惠险,你說(shuō)我怎么就攤上這事苗傅。” “怎么了班巩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵渣慕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我抱慌,道長(zhǎng)摇庙,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任卫袒,我火速辦了婚禮夕凝,結(jié)果婚禮上户秤,老公的妹妹穿的比我還像新娘。我一直安慰自己转砖,他們只是感情好鲸伴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布汞窗。 她就那樣靜靜地躺著,像睡著了一般不铆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上誓斥,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天劳坑,我揣著相機(jī)與錄音,去河邊找鬼析珊。 笑死蔑穴,一個(gè)胖子當(dāng)著我的面吹牛惧浴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捐腿,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼柿顶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了宪祥?” 一聲冷哼從身側(cè)響起家乘,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤仁锯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后野芒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體双炕,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡雄家,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年胀滚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咽笼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戚炫。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡双肤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出七芭,到底是詐尸還是另有隱情蔑赘,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布耙箍,位于F島的核電站酥馍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏汁针。R本人自食惡果不足惜峦失,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一尉辑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卓练,春花似錦购啄、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蔚龙。三九已至,卻和暖如春甲雅,著一層夾襖步出監(jiān)牢的瞬間坑填,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工妖枚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚪黑,地道東北人中剩。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓结啼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親郊愧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353