[譯]為什么使用MVI模式(MVI編寫響應式安卓APP入門系列第一部分MODEL)

我曾經(jīng)有一個瞬間覺的我的Model定義全都是錯的。經(jīng)過在各種安卓開發(fā)論壇也好主題也罷的討論和頭疼的研究啡浊。無論如何岂座,最終我選擇使用rxjava和Model-View-Intent(MVI)的方式構(gòu)建響應式的安卓應用程序肪凛,就像這種組合我以前是沒有嘗試過一樣,我創(chuàng)建是十分被動的辽社。當然伟墙,你也會,但是滴铅,你會比我好很多戳葵,因為,我將寫一系列文章來介紹這個模式和用法汉匙。在第一節(jié)拱烁,也就是這篇文章生蚁,我們來說說我們的Model出現(xiàn)了什么問題?

我為什么說我以前定義的Model全都是錯的咧?誠然戏自,有很多模式將"View"和"Model"分離邦投。在安卓開發(fā)領(lǐng)域,最出名的當屬Model-View-Controller(MVC),Model-View_Presenter(MVP)和Model-View-ViewModel(MVVM)擅笔。你可以從名字看出什么東西么志衣?他們都有Model。但是猛们,我發(fā)現(xiàn)大多數(shù)時間念脯,我根本沒有用Model。

例子:僅僅是在后臺加載一個persons的列表弯淘,一個傳統(tǒng)的MVP模式的代碼是這樣的:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().showLoading(true); // 顯示一個加載進度條

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

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

但是到底什么是"Model"?后臺運行是Model?不是绿店,Model應當是業(yè)務邏輯。它是作為結(jié)果的列表庐橙?不是假勿,它僅僅只做一件事情,就是我們View顯示所需要的東西怕午,像加載指示器或錯誤信息废登。因此,真正的Model“長”什么樣的郁惜?
如果按照我對Model的理解堡距,那么,Model類應當是這樣的:

class PersonsModel {
  // 在真實的項目中兆蕉,需要定義為私有的
  // 并且我們需要通過getter和setter來訪問它們
  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;
  }
}

那么Presenter應該“長”這樣的:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().render( new PersonsModel(true, null, null) ); // 顯示一個加載進度條

    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) ); // 顯示錯誤信息
      }
    });
  }
}

現(xiàn)在在屏幕上的View有了一個將被渲染上去的Model羽戒。這個概念其實不是什么新概念。最開始的被Trygve Reenskaug在1979年定義的MVC模式的Model的定義幾乎一致:View觀察Model的變化虎韵。不幸的是易稠,MVC這個術(shù)語被濫用來描述太多不同的模式,它們與最原始的MVC定義有了出入包蓝。例如驶社,后端工程師使用MVC框架,iOS工程師有ViewController测萎,在安卓開發(fā)中MVC的真正含義是什么亡电?Activities是Controller?那么ClickListener意味著什么?現(xiàn)在MVC與最初被Reenskaug定義的MVC來講,這個術(shù)語被誤解硅瞧,濫用和錯誤使用份乒。關(guān)于MVC的討論就此打住,在討論下去文章就要失控翻車了。

讓我們回到我剛開始說的地方或辖。Model需要解決我們在安卓開發(fā)中經(jīng)常遇到的問題:

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

讓我們討論的上面這些點瘾英,并研究傳統(tǒng)的MVP和MVVM如何處理這些內(nèi)容,最后颂暇,在探究到底什么樣的Model可以幫助避免共性的陷阱缺谴。

1.狀態(tài)問題

響應式App,可以說最近非常流行蟀架。難道不是么瓣赂?所謂的響應式App應該就是會根據(jù)應用的狀態(tài)改變,來改變UI片拍。這里還有一個單詞:"State(上文譯為狀態(tài))"煌集。什么是"State(上文譯為狀態(tài))"?大多數(shù)時間我們描述“State(上文譯為狀態(tài))”,就是我們從屏幕上看到的東西捌省,比如說在屏幕上顯示一個ProgressBar 就是“加載狀態(tài)”苫纤。最關(guān)鍵的地方:我們的前端開發(fā)者趨向于關(guān)注UI。這明顯不是一件壞事纲缓,因為一個好的UI決定了用戶會不會用你們家的產(chǎn)品卷拘,從而決定了產(chǎn)品能不能成功。但是祝高,我們看一下上面最基本的MVP示例代碼(不是用PersonsModel的例子栗弟,是最上面的例子)。Ui的狀態(tài)被Presenter協(xié)調(diào)工闺,Presenter決定了View應該顯示什么內(nèi)容乍赫。MVVM也是同樣的。在這篇博客中我簡單區(qū)分兩種MVVM實現(xiàn):第一種是用到了Android的data binding陆蟆,第二種是用到RxJava雷厂。在用data binding實現(xiàn)的MVVM這種方式下,狀態(tài)直接被定義到了ViewModel里面:

class PersonsViewModel {
  ObservableBoolean loading;
  // ... Other fields left out for better readability

  public void load(){

    loading.set(true);

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
      loading.set(false);
      // ... other stuff like set list of persons
      }

      public void onError(Throwable error){
        loading.set(false);
        // ... other stuff like set error message
      }
    });
  }
}

在使用RxJava實現(xiàn)的MVVM中叠殷,我們不需要使用data binding引擎改鲫,而是將Observable綁定到View中的UI Widget,例如:

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)
      // Could also be implemented entirely different
  }

  // Subscribed to in View (i.e. Activity / Fragment)
  public Observable<Boolean> loading(){
    return loading;
  }

  // Subscribed to in View (i.e. Activity / Fragment)
  public Observable<List<Person>> persons(){
    return persons;
  }

  // Whenever this action is triggered (calling onNext() ) we load persons
  public PublishSubject loadPersonsCommand(){
    return loadPersonsCommand;
  }
}

當然林束,這只是一個代碼片段不是一個完整的代碼像棘,你實現(xiàn)的可能看起來完全不一樣。重點是通常在MVP和MVVM中壶冒,狀態(tài)由Presenter或ViewModel驅(qū)動缕题。
這導致了下面幾個問題:

  1. 業(yè)務邏輯有了自己的狀態(tài),Presenter(或ViewModel)有了自己的狀態(tài)(你需要同步你的業(yè)務邏輯狀態(tài)依痊,和你的Presenter的狀態(tài)避除,兩者需要保持一致)并且View可能也有自己的狀態(tài)(舉個栗子,您直接在視圖中設置可見性胸嘁,或者Android本身在從bundle中恢復狀態(tài))
  2. Presenter(或者ViewModel)有任意多的輸入(View的觸發(fā)瓶摆,被Presenter處理),這是可以理解的,但是Presenter也有很多的輸出(或輸出一些像 view.showLoading()view.showError() 在MVP或ViewModel都提供觀察)那么這種情況會導致View,Presenter和業(yè)務邏輯的狀態(tài)沖突性宏,這種現(xiàn)象在多線程下尤為突出群井。

在最好的情況下,這只會導致可見的錯誤毫胜,例如像這樣同時顯示加載指示符(“加載狀態(tài)”)和錯誤指示符(“錯誤狀態(tài)”)书斜。

plaid app.gif

在最壞的情況下,你有一個像Crashlytics(理解成bugly)這樣的崩潰報告工具報告給你的嚴重的錯誤酵使,你無法重現(xiàn)荐吉,因此幾乎不可能修復。

如果口渔,我們從底層(業(yè)務邏輯)到頂層(VIew)有且僅有一個狀態(tài)源样屠。其實,我們最開始展示的第二個例子就是一個很接近這個概念的例子缺脉。

class PersonsModel {
  // 在真實的項目中痪欲,需要定義為私有的
  // 并且我們需要通過getter和setter來訪問它們
  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;
  }
}

你猜怎么了? 模型反應了狀態(tài) 。當我理解了這個攻礼,那么多個狀態(tài)依賴的問題就被解決了(從一開始就阻止了)业踢,并且我的Presenter也就只有一個明確的輸出:getView().render(PersonsModel) .這反應了一個簡單的數(shù)學函數(shù)像f(x)=y (也可以有多個輸入,例如f(a,b,c),但只有一個輸出)礁扮。數(shù)學并不是所有的人都擅長知举,但是,數(shù)學家不知道什么是Bug深员。軟件工程師咧负蠕。

理解什么是"Model",并且知道m(xù)odel如何正確的定義倦畅,是十分重要的遮糖,因為到最后Model將解決"狀態(tài)問題"。

2.屏幕方向改變

安卓屏幕方向改變是一個有挑戰(zhàn)性性的問題叠赐。最簡單的方法是直接忽略這個問題欲账。當屏幕方向改變的時候,重新加載所有的東西芭概。這是完全有效的解決方法赛不。大多數(shù)時間,你的App在離線狀態(tài)下工作罢洲,數(shù)據(jù)是存儲在你的本地數(shù)據(jù)庫或者其他的本地緩存踢故。因此文黎,當屏幕的方向發(fā)生改變,加載數(shù)據(jù)是很快的殿较、然而耸峭,我個人不喜歡看到loading指示器(大神都是有點各種小脾氣的),盡管它可能只出現(xiàn)幾微秒的時間(這里應該用了夸張的修辭手法)淋纲,因為在我看來這不是一個無縫的用戶體驗劳闹。因此,很多人(包括我)開始使用帶有“固定的presenter”的MVP洽瞬。因此View可以在屏幕方向旋轉(zhuǎn)的時候被分離(被銷毀)本涕,而presenter將被保留在內(nèi)存中,隨后伙窃,我們的View和Presenter將會被重新連接菩颖。在用RxJava實現(xiàn)的MVVM中有相同的概念,但是为障,我們需要記在心中的是一旦View被它的ViewModel退訂那么觀察流就被破壞位他。例如,你可以用Subjects來解決這個問題产场。在使用data binding實現(xiàn)的MVVM中ViewModel是通過data binding 引擎直接綁定在View上的鹅髓。去避免當我們改變屏幕方向而導致的內(nèi)存泄露。

但是固定的Presenter(或者ViewModel)有一個問題是:當屏幕旋轉(zhuǎn)的時候京景,我們?nèi)绾螌iew的狀態(tài)退回旋轉(zhuǎn)前的狀態(tài)窿冯,也就是說,我們的View和Presenter是否處在相同的狀態(tài)确徙?我寫了一個MVP庫叫做Mosby 帶有一個功能叫做ViewState ,用來同步業(yè)務邏輯和View的狀態(tài)醒串。Moxy ,另一個MVP庫,用了一種有趣的方式解決了這個問題鄙皇,解決的方法就是用到了"命令(原文為commands)"去在屏幕旋轉(zhuǎn)以后芜赌,重建View的狀態(tài):

moxy.gif

我可以十分確定的是,肯定有其他方法來解決這個問題伴逸。讓我們退一步來說缠沈,我們總結(jié)一下上面說到的庫的解決方法:他們試圖解決我們一直在討論的狀態(tài)問題。

所以错蝴,再次強調(diào)洲愤,當有一個能夠反應確切的"狀態(tài)"的"Model",肯定只有一個方法去"渲染(原文為render)"這個"Model"解決這個問題顷锰,并且是通過一種簡單的如調(diào)用 getView().render(PersonsModel) 一樣柬赐。

3.在頁面堆棧中導航

Presenter(或者ViewModel)需要去維護什么時候View不使用么?舉個栗子,如果官紫,F(xiàn)ragment(View)將被另一個Fragment替換掉肛宋,因為用戶導航到另外的頁面州藕,那么這個將沒有View附屬到Presenter里。如果沒有View沒有Presenter顯然不可能用最新的從業(yè)務邏輯里出來的數(shù)據(jù)去更新View酝陈。如果用戶返回(例如慎框,用戶按了返回按鈕)?去重新加載數(shù)據(jù)或復用已經(jīng)存在的Presenter?這是一道哲學題后添。通常的一旦用戶返回先前的頁面,他期望回到他原來閱讀的地方薪丁。這是個最基本的“重置View狀態(tài)的問題”遇西,我們剛剛在2中也討論了這個問題。所以富有策略的解決方案:當"Model"代表一種狀態(tài)严嗜,我們僅僅需要當用戶返回時粱檀,調(diào)用getView().render(PersonModel) 去渲染視圖就可以了。

4.進程死亡

我認為這是個安卓開發(fā)普遍錯誤的理解漫玄,就是進程死亡是意見壞的事情茄蚯,并且在進程死亡以后,我們需要庫去幫助我們重啟狀態(tài)(例如Presenters或者ViewModels)睦优。第一渗常,一個進程死亡發(fā)生的原因是:安卓操作系統(tǒng)需要更多的資源去給其他的App或者為了省電。但是汗盘,如果你的應用程序處于前臺皱碘,正在被你的用戶使用是決定不可能出現(xiàn)進程死亡的。因此隐孽,做個好市民癌椿,不要在和平臺對戰(zhàn)了(這里的意思是不要再瞎折騰進程包活了)。如果你真的需要在后臺長時間運行的一些工作,請用 Service 菱阵,在安卓操作系統(tǒng)中踢俄,這是唯一的一種方式向系統(tǒng)發(fā)出你的應用程序仍然被使用的信號。如果一個進程死亡發(fā)生晴及,安卓提供了一些回調(diào)像onSaveInstanceState() 去保存狀態(tài)都办。State又出現(xiàn)了。我們應該保存我們的View信息到Bundle里么?我們的Presenter的狀態(tài)是不是也要存儲到Bundle里?那么業(yè)務邏輯的狀態(tài)要不要存?我們先前也一直討論這個問題:剛才1.2.3點都在討論這個問題虑稼。我們僅僅需要一個Model類脆丁,這個Model類代表了整個狀態(tài)。那么存儲到Bundle里动雹,就變得很簡單了槽卫。然而,我個人意見認為大多數(shù)時間我們不存儲狀態(tài)數(shù)據(jù)胰蝠,而選擇像我們啟動App時候重新加載整個屏幕歼培,似乎更好震蒋。考慮一下新聞閱讀軟件顯示新聞列表躲庄,當我們App六小時以前被殺死查剖,我們存儲了的新聞狀態(tài),當用戶重新打開我們的App的時候噪窘,我們六小時前存儲的狀態(tài)被重新顯示出來笋庄,很顯然新聞已經(jīng)過期了。也許在這種場景下倔监,不去存儲狀態(tài)(Model/State)直砂,而去重新加載數(shù)據(jù)是更好的選擇。

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

我這里不去討論不變性(immutabiliy)的先進性浩习,因為有很多資源討論這個問題静暂。我們需要一個不變的“Model”(代表狀態(tài))。為啥谱秽?因為我們想要唯一的來源洽蛀。當我們傳遞Model對象的時候,我們不想要在我們應用中其他組件去改變我們的Model/狀態(tài)(State)疟赊。讓我想象一下我們正在寫一個簡單的“計數(shù)器”的安卓應用程序郊供,這個有一個增量和一個減量按鈕,并且在一個TextView中顯示當前技術(shù)的值近哟。如果我們的Model(就是計數(shù)的值颂碘,一個Integer)是不可變的,我們?nèi)绾稳ジ挠嫈?shù)器椅挣?我要告訴你头岔,我們不直接通過按鈕點擊來控制TextView。一些建議:第一鼠证,我們的View應該有一個view.render(...).第二峡竣,我們的Model是不可變的,因此不可能直接修改Model量九。第三适掰,有且只有一個來源:業(yè)務邏輯。我們讓點擊事件“下沉”到業(yè)務邏輯層荠列。業(yè)務邏輯知道了當前的Model(例如类浪,當前Model有一個私有域)并且將根據(jù)舊的Model,創(chuàng)建一個新的帶有增量/減量值的Model肌似。

counter.png

通過這樣做费就,我們確信有單向的數(shù)據(jù)流,并將業(yè)務邏輯作為創(chuàng)建不可變模型實例的單一的來源川队。對于一個計數(shù)器來講有點太小題大做力细。難道不是么睬澡?是的,一個計數(shù)器是一個非常簡單的程序眠蚂。大多數(shù)的App都是從一個簡單的程序變的復雜起來煞聪。我認為,一個單向的數(shù)據(jù)流和一個不變的Model是十分必要的逝慧,當我們工程變復雜的時候昔脯,開發(fā)將依然是簡單的。

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

此外笛臣,單向數(shù)據(jù)流保證了我們的APP的調(diào)試非常簡單云稚。下次如果有新的crash報告從Crashlytics(感覺類似與Bugly)傳過來,我們可以很快速的修復Crash捐祠。因為所有需要的信息都會在crash報告里面。什么是“需要的信息”桑李?就是我們需要的當前Model和用戶執(zhí)行了什么樣的操作而導致的八阿哥(例子:點擊減量按鈕)踱蛀。這就是我們需要的信息,而且這些信息很顯然贵白,是十分容易附加到Crash報告中的率拒。如果,數(shù)據(jù)流不是單向的禁荒,那么實現(xiàn)起來就有點困難猬膨。(例如:一些人亂用EventBus,并且將CounterModels暴露出來。譯者:EventBus沒用過呛伴,所以勃痴,這里可能看起來怪怪的原話是someone misuses an EventBus and fires CounterModels out into the wild )或不具有不變性(這樣會導致我們不能確定誰改變了Model)。

7. 可測試

"傳統(tǒng)"MVP或者MVVM改善了應用程序的可測試性热康。MVC也是可測試的:沒人告訴我們業(yè)務邏輯一定要放在activity里沛申。當Model代表狀態(tài),我們可以簡化我們的集成測試代碼姐军,例如铁材,我們可以簡單的檢查assertEquals(expectedModel, model) 。這讓我們除了Model以外的所有對象都不用mock奕锌。另外著觉,這可以消除了方法的許多驗證測試,例如 Mockito.verify(view, times(1)).showFoo() 惊暴。最后饼丘,它可以讓我們的測試代碼可讀性更好,更容易理解辽话,更好的可維護性葬毫,我們不需要糾結(jié)于如何實現(xiàn)在代碼中實現(xiàn)一些細節(jié)镇辉。

總結(jié)

作為這個系列的第一篇博客,我們討論了很多關(guān)于理論的東西贴捡。我們真的需要花4千多字介紹Model么?我認為理解Model的實現(xiàn)是十分重要的基礎(chǔ)忽肛,有助于防止一些問題,否則容易翻車烂斋。Model不意味著業(yè)務邏輯屹逛,它是生成Model的業(yè)務邏輯(例如,一個交互汛骂,一個用例罕模,一個倉庫或者你在APP中調(diào)用的任何東西(原文:Model doesn’t mean business logic. It’s the business logic (i.e. an Interactor, a Usecase, a Repositor or whatever you call it in your app) that produces a Model.)。在第二部分帘瞭,我們將要將我們學到的Model理論淑掌,用到Model-View-Intent上,來構(gòu)建響應式應用程序蝶念。下面展示的抛腕,簡單的在線商城軟件將是我們以后去實現(xiàn)的一個例子。你可以期望在第二部分了媒殉。 敬請關(guān)注担敌。


shopDemo.gif
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市廷蓉,隨后出現(xiàn)的幾起案子全封,更是在濱河造成了極大的恐慌,老刑警劉巖桃犬,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刹悴,死亡現(xiàn)場離奇詭異,居然都是意外死亡攒暇,警方通過查閱死者的電腦和手機颂跨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扯饶,“玉大人恒削,你說我怎么就攤上這事∥残颍” “怎么了钓丰?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長每币。 經(jīng)常有香客問我携丁,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任梦鉴,我火速辦了婚禮李茫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肥橙。我一直安慰自己魄宏,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布存筏。 她就那樣靜靜地躺著宠互,像睡著了一般。 火紅的嫁衣襯著肌膚如雪椭坚。 梳的紋絲不亂的頭發(fā)上予跌,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音善茎,去河邊找鬼券册。 笑死,一個胖子當著我的面吹牛垂涯,可吹牛的內(nèi)容都是我干的烁焙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼集币,長吁一口氣:“原來是場噩夢啊……” “哼考阱!你這毒婦竟也來了翠忠?” 一聲冷哼從身側(cè)響起鞠苟,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秽之,沒想到半個月后当娱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淘太,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡皇钞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了躺酒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片河质。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡冀惭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掀鹅,到底是詐尸還是另有隱情散休,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布乐尊,位于F島的核電站戚丸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扔嵌。R本人自食惡果不足惜限府,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一夺颤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胁勺,春花似錦世澜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蛇捌,卻和暖如春抚恒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背络拌。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工俭驮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人春贸。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓混萝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親萍恕。 傳聞我的和親對象是個殘疾皇子逸嘀,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容