[譯]使用MVI打造響應(yīng)式APP(二):View層和Intent層

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

上文 中蛋逾,我們探討了對Model的定義遇汞、與 狀態(tài) 的關(guān)系以及如何在通過良好地定義Model來解決一些Android開發(fā)中常見的問題辱姨。本文將通過 Model-View-Intent 憔鬼,即MVI模式,繼續(xù)我們的 響應(yīng)式App 構(gòu)建之旅洋只。

如果您尚未閱讀上一小節(jié)样眠,則應(yīng)在繼續(xù)閱讀本文之前閱讀該部分∪ㄉ眨總結(jié)一下:以“傳統(tǒng)的”MVP為例眯亦,請避免寫出這樣的代碼:

class PersonsPresenter extends Presenter<PersonsView> {

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

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

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

我們應(yīng)該創(chuàng)建能夠反映 狀態(tài)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層也應(yīng)該像這樣進行定義:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().render( new PersonsModel(true, null, null) ); // 展示一個 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) ); // 展示錯誤信息
      }
    });
  }
}

現(xiàn)在,僅需簡單調(diào)用View層的render(personsModel)方法板祝,Model就會被成功的渲染在屏幕上宫静。在第一小節(jié)中我們同樣探討了 單項數(shù)據(jù)流 的重要性,同時您的業(yè)務(wù)邏輯應(yīng)該驅(qū)動該Model券时。在正式將所有內(nèi)容環(huán)環(huán)相扣連接之前孤里,我們先簡單了解一下MVI的核心思想。

Model-View-Intent (MVI)

該模式最初被 andrestaltz 在他寫的JavaScript框架 cycle.js 中所提出; 從理論(還有數(shù)學(xué))上講橘洞,我們這樣對Model-View-Intent的定義進行描述:

[站外圖片上傳中...(image-fc6580-1552228859520)]

1.intent()

此函數(shù)接受來自用戶的輸入(即UI事件捌袜,比如點擊事件)并將其轉(zhuǎn)換為可傳遞給Model()函數(shù)的參數(shù),該參數(shù)可能是一個簡單的StringModel進行賦值炸枣,也可能像是Object這樣復(fù)雜的數(shù)據(jù)結(jié)構(gòu)虏等。intent作為意圖,標志著 我們試圖對Model進行改變抛虏。

2.model()

model()函數(shù)將intent()函數(shù)的輸出作為輸入來操作Model博其,其函數(shù)輸出是一個新的Model(狀態(tài)發(fā)生了改變)。

不要對已存在的Model對象進行修改迂猴,我們需要的是不可變慕淡!對此,在上文中我們已經(jīng)展示了一個計數(shù)器的具體案例沸毁,再次重申峰髓,不要修改已存在的Model傻寂!

根據(jù)intent所描述的變化,我們創(chuàng)建一個新的Model,請注意携兵,Model()函數(shù)是唯一允許對Model進行創(chuàng)建的途徑疾掰。然后這個新的Model作為該函數(shù)的輸出——基本上model()函數(shù)調(diào)用我們App的業(yè)務(wù)邏輯(可以是交互、用例徐紧、Repository......您在App中使用的任何模式/術(shù)語)并作為結(jié)果提供新的Model對象静檬。

3.view()

該方法獲取model()函數(shù)返回的Model,并將其作為view()函數(shù)的輸入并级,這之后通過某種方式將Model展示出來拂檩,view()view.render(model)大體上是一致的。

4.本質(zhì)

但是我們希望構(gòu)建的是 響應(yīng)式的App嘲碧,不是嗎稻励?那么MVI是如何響應(yīng)式的呢?響應(yīng)式實際上意味著什么愈涩?

這意味著AppUI反映了狀態(tài)的變更望抽。

因為Model反映了狀態(tài),因此履婉,本質(zhì)上我們希望 業(yè)務(wù)邏輯能夠?qū)斎氲氖录?code>intents)進行響應(yīng)煤篙,并創(chuàng)建對應(yīng)的Model作為輸出,這之后再通過調(diào)用View層的render(model)方法毁腿,對UI進行渲染舰蟆。

5.通過RxJava串聯(lián)

我們希望我們的數(shù)據(jù)流的單向性,因此RxJava閃亮登場狸棍。我們的App必須通過RxJava保持 數(shù)據(jù)的單向性響應(yīng)式 來構(gòu)建嗎?或者必須用MVI模式才能構(gòu)建嗎味悄?當(dāng)然不草戈,我們也可以寫 命令式程序性 的代碼。但是侍瑟,基于事件編程RxJava實在太優(yōu)秀了唐片,既然UI是基于事件的,因此使用RxJava也是非常有意義的涨颜。

本文我們將會構(gòu)建一個簡單的虛擬在線商店App费韭,其UI界面中展示的商品數(shù)據(jù),都來源于我們向后臺進行的網(wǎng)絡(luò)請求庭瑰。

我們可以精確的搜索特定的商品星持,并將其添加到我們的購物車中,最終App的效果如下所示:

這個項目的源碼你可以在Github上找到弹灭,我們從實現(xiàn)一個簡單的搜索界面開始做起:

首先督暂,就像上文我們描述的那樣揪垄,我們定義一個Model用于描述View層是如何被展示的—— 這個系列中,我們將用帶有 ViewState 后綴的類來替代 Model逻翁;舉個例子饥努,我們將會為搜索頁的Model類命名為SearchViewState

這很好理解八回,因為Model反應(yīng)的就是狀態(tài)(State)酷愧,至于為什么不用聽起來有些奇怪的名稱比如SearchModel,是因為擔(dān)心和MVVM中的SearchViewModel類在一起會導(dǎo)致歧義——命名真的很難缠诅。

public interface SearchViewState {

  // 搜索尚未開始
  final class SearchNotStartedYet implements SearchViewState {
  }

  // 搜索中
  final class Loading implements SearchViewState {
  }

  // 返回結(jié)果為空
  final class EmptyResult implements SearchViewState {
    private final String searchQueryText;

    public EmptyResult(String searchQueryText) {
      this.searchQueryText = searchQueryText;
    }

    public String getSearchQueryText() {
      return searchQueryText;
    }
  }

  // 有效的搜索結(jié)果溶浴,包含和搜索條件匹配的商品列表
  final class SearchResult implements SearchViewState {
    private final String searchQueryText;
    private final List<Product> result;

    public SearchResult(String searchQueryText, List<Product> result) {
      this.searchQueryText = searchQueryText;
      this.result = result;
    }

    public String getSearchQueryText() {
      return searchQueryText;
    }

    public List<Product> getResult() {
      return result;
    }
  }

  // 表示搜索過程中發(fā)生了錯誤
  final class Error implements SearchViewState {
    private final String searchQueryText;
    private final Throwable error;

    public Error(String searchQueryText, Throwable error) {
      this.searchQueryText = searchQueryText;
      this.error = error;
    }

    public String getSearchQueryText() {
      return searchQueryText;
    }

    public Throwable getError() {
      return error;
    }
  }
}

因為Java是一種強類型的語言,因此我們可以選擇一種安全的方式為我們的Model類拆分出多個不同的 子狀態(tài)滴铅。

我們的業(yè)務(wù)邏輯返回的是一個 SearchViewState 類型的對象戳葵,它可能是SearchViewState.Error或者其它的一個實例。這只是我個人的偏好汉匙,們也可以通過不同的方式定義拱烁,例如:

class SearchViewState {
  Throwable error;  // 非空則意味著,出現(xiàn)了一個錯誤
  boolean loading;  // 值為true意味著正在加載中
  List<Product> result; // 非空意味著商品列表的結(jié)果
  boolean SearchNotStartedYet; // true意味著還未開始搜索
}

再次重申噩翠,如何定義Model純屬個人喜好戏自,如果你用Kotlin作為編程語言,那么sealed classes是一個不錯的選擇伤锚。

將目光聚集回到業(yè)務(wù)代碼擅笔,讓我們通過 SearchInteractor 去執(zhí)行搜索的功能,其輸出就是我們之前說過的SearchViewState對象:

public class SearchInteractor {
  final SearchEngine searchEngine; // 執(zhí)行網(wǎng)絡(luò)請求

  public Observable<SearchViewState> search(String searchString) {
    // 如果是空的字符串屯援,不進行搜索
    if (searchString.isEmpty()) {
      return Observable.just(new SearchViewState.SearchNotStartedYet());
    }

    // 搜索商品列表
    // 返回 Observable<List<Product>>
    return searchEngine.searchFor(searchString)
        .map(products -> {
          if (products.isEmpty()) {
            return new SearchViewState.EmptyResult(searchString);
          } else {
            return new SearchViewState.SearchResult(searchString, products);
          }
        })
        .startWith(new SearchViewState.Loading())
        .onErrorReturn(error -> new SearchViewState.Error(searchString, error));
  }
}

來看下SearchInteractor.search()的方法簽名:我們將String類型的searchString作為 輸入 的參數(shù)猛们,以及Observable<SearchViewState>類型的 輸出,這意味著我們期望隨著時間的推移狞洋,可以在可觀察的流上會有任意多個SearchViewState的實例被發(fā)射弯淘。

在我們正式開始查詢搜索之前(即SearchEngine執(zhí)行網(wǎng)絡(luò)請求),我們通過startWith()操作符發(fā)射一個SearchViewState.Loading,這將會使得View在執(zhí)行搜索時展示ProgressBar吉懊。

onErrorReturn()會捕獲在執(zhí)行搜索時拋出的所有異常庐橙,并且發(fā)射出一個SearchViewState.Error——在訂閱這個Observable時,我們?yōu)槭裁床蝗ナ褂?code>onError()回調(diào)呢借嗽?

這是一個對RxJava認知的普遍誤解态鳖,實際上,onError()的回調(diào)意味著 整個可觀察的流進入了不可恢復(fù)的狀態(tài)恶导,因此可觀察的流結(jié)束了浆竭,而在我們的案例中,類似“沒有網(wǎng)絡(luò)連接”的error并非不可恢復(fù)的error:這只是我們的Model所代表的另外一個狀態(tài)。

此外兆蕉,我們還有另外一個可以轉(zhuǎn)換到的狀態(tài)羽戒,即一旦網(wǎng)絡(luò)連接可用,我們可以通過 SearchViewState.Loading 跳轉(zhuǎn)到的 加載狀態(tài)虎韵。

因此易稠,我們建立了一個可觀察的流,這是一個每當(dāng)狀態(tài)發(fā)生了改變包蓝,從業(yè)務(wù)邏輯層就會發(fā)射一個發(fā)生了改變的ModelView層的流驶社。

我們不想在網(wǎng)絡(luò)連接錯誤時終止這個可觀察的流,因此测萎,在error發(fā)生時亡电,類似這種可以被處理為 狀態(tài)error(而不是終止流的那種致命的錯誤),可以反應(yīng)為Model硅瞧,被可觀察的流發(fā)射份乒。

通常,在MVI中腕唧,ModelObservable永遠不會被終止(即永遠不會執(zhí)行onComplete()或者onError()回調(diào))或辖。

總結(jié)一下,SearchInteractor(即業(yè)務(wù)邏輯)提供了一個可觀察的流Observable<SearchViewState>枣接,每當(dāng)狀態(tài)發(fā)生了變化颂暇,就會發(fā)射一個新的SearchViewState

6.View層的職責(zé)

接下來我們來討論一下View應(yīng)該是什么樣的但惶,View層的職責(zé)是什么耳鸯?顯然View層應(yīng)該對Model進行展示,我們已經(jīng)認可View層應(yīng)該有類似 render(model) 這樣的函數(shù)膀曾。此外县爬,View應(yīng)該提供一個給其他層響應(yīng)用戶輸入的方法,在MVI中這個方法被稱為 intents添谊。

在這個案例中捌省,我們只有一個intent:用戶可以在輸入框中輸入一個用于檢索商品的字符串進行搜索。MVP中的好習(xí)慣是為View層定義一個接口碉钠,所以在MVI中我們也可以這樣做。

public interface SearchView {

  // 搜索的intent
  Observable<String> searchIntent();

  // 對View層進行渲染
  void render(SearchViewState viewState);
}

我們的案例中View層只提供了一個intent,但通常View擁有更多的intent;在 第一小節(jié) 中我們討論了為什么一個單獨的render()函數(shù)是一個不錯的實踐卷拘,如果你對此還不是很清楚的話喊废,請閱讀該小節(jié)并通過留言進行探討。

在我們開始對View層進行具體的實現(xiàn)之前栗弟,我們先看看最終界面的展示效果:

public class SearchFragment extends Fragment implements SearchView {

  @BindView(R.id.searchView) android.widget.SearchView searchView;
  @BindView(R.id.container) ViewGroup container;
  @BindView(R.id.loadingView) View loadingView;
  @BindView(R.id.errorView) TextView errorView;
  @BindView(R.id.recyclerView) RecyclerView recyclerView;
  @BindView(R.id.emptyView) View emptyView;
  private SearchAdapter adapter;

  @Override public Observable<String> searchIntent() {
    return RxSearchView.queryTextChanges(searchView) // 感謝 Jake Wharton :)
        .filter(queryString -> queryString.length() > 3 || queryString.length() == 0)
        .debounce(500, TimeUnit.MILLISECONDS);
  }

  @Override public void render(SearchViewState viewState) {
    if (viewState instanceof SearchViewState.SearchNotStartedYet) {
      renderSearchNotStarted();
    } else if (viewState instanceof SearchViewState.Loading) {
      renderLoading();
    } else if (viewState instanceof SearchViewState.SearchResult) {
      renderResult(((SearchViewState.SearchResult) viewState).getResult());
    } else if (viewState instanceof SearchViewState.EmptyResult) {
      renderEmptyResult();
    } else if (viewState instanceof SearchViewState.Error) {
      renderError();
    } else {
      throw new IllegalArgumentException("Don't know how to render viewState " + viewState);
    }
  }

  private void renderResult(List<Product> result) {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.VISIBLE);
    loadingView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    adapter.setProducts(result);
    adapter.notifyDataSetChanged();
  }

  private void renderSearchNotStarted() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderLoading() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.VISIBLE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderError() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.VISIBLE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderEmptyResult() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.VISIBLE);
  }
}

render(SearchViewState)方法的作用顯而易見污筷,searchIntent()方法中,我們使用了Jake WhartonRxBinding ,這是一個對Android UI組件提供了RxJava響應(yīng)式支持的庫瓣蛀。

RxSearchView.queryText()創(chuàng)建了一個Observable<String>,每當(dāng)用戶在EditText上輸入了一些文字陆蟆,它就會發(fā)射一個對應(yīng)的字符串;我們通過filter()去保證只有當(dāng)用戶輸入的字符數(shù)達到三個以上時才進行搜索惋增;同時叠殷,我們不希望每當(dāng)用戶輸入一個字符,就去請求網(wǎng)絡(luò)诈皿,而是當(dāng)用戶輸入結(jié)束后再去請求網(wǎng)絡(luò)(debounce()操作符會停留500毫秒以決定用戶是否輸入完成)林束。

現(xiàn)在我們知道了屏幕中的searchIntent()方法就是 輸入 ,而render()方法則是 輸出稽亏。我們?nèi)绾螐?輸入 獲得 輸出 呢壶冒,如下所示:

7.連接View和Intent

剩下的問題就是:我們?nèi)绾螌?code>View的intent和業(yè)務(wù)邏輯進行連接呢?如果你認真觀看了上面的流程圖截歉,你應(yīng)該注意到了中間的 flatMap() 操作符胖腾,這暗示了我們還有一個尚未談及的組件: Presenter ;Presenter負責(zé)連接這些點,就和我們在MVP中使用的方式一樣瘪松。

public class SearchPresenter extends MviBasePresenter<SearchView, SearchViewState> {
  private final SearchInteractor searchInteractor;

  @Override protected void bindIntents() {
    Observable<SearchViewState> search =
        intent(SearchView::searchIntent)
        // 上文中我們談到了flatMap,但在這里switchMap更為適用
            .switchMap(searchInteractor::search)
            .observeOn(AndroidSchedulers.mainThread());

    subscribeViewState(search, SearchView::render);
  }
}

什么是 MviBasePresenter, intent()subscribeViewState() 又是什么咸作?這個類是我寫的 Mosby 庫的一部分(3.0版本后,Mosby已經(jīng)支持了MVI)凉逛。本文并非為了講述Mosby性宏,但我向簡單介紹一下MviBasePresenter是如何的便利——這其中沒有什么黑魔法,雖然確實看起來像是那樣状飞。

讓我們從生命周期開始:MviBasePresenter并未持有任何生命周期毫胜,它暴露出一個 bindIntent() 方法以供View層和業(yè)務(wù)邏輯進行綁定。通常诬辈,你通過flatMap()酵使、switchMap()或者concatMap()操作符將intent “轉(zhuǎn)移”到業(yè)務(wù)邏輯中,這個方法僅僅在View層第一次被附加到Presenter中時調(diào)用焙糟,而當(dāng)View再次被附加在Presenter中時(比如口渔,屏幕方向發(fā)生了改變),將不再被調(diào)用穿撮。

這聽起來有些怪缺脉,也許有人會說:

MviBasePresenter在屏幕方向發(fā)生了改變后依然能夠存活?如果是這樣悦穿,Mosby如何保證Observable的流不會發(fā)生內(nèi)存的泄漏攻礼?

這就是 intent()subscribeViewState() 的作用所在了,intent() 在內(nèi)部創(chuàng)建一個PublishSubject栗柒,就像是業(yè)務(wù)邏輯的“網(wǎng)關(guān)”一樣礁扮;實際上,PublishSubject訂閱了View層傳過來的intentObservable,調(diào)用intent(o1)實際返回了一個訂閱了o1PublishSubject

屏幕發(fā)生旋轉(zhuǎn)時,MosbyViewPresenter中分離太伊,但是雇锡,內(nèi)部的PublishSubject只是暫時和View解除了訂閱;而當(dāng)View重新附著在Presenter上時僚焦,PublishSubject將會對View層的intent進行重新訂閱锰提。

subscribeViewState()方法做的是同樣的事情,只不過將順序調(diào)換了過來(PresenterView層的通信)叠赐。它在內(nèi)部創(chuàng)建一個BehaviorSubject作為從業(yè)務(wù)邏輯到View層的“網(wǎng)關(guān)”欲账。

由于它是一個BehaviorSubject,因此芭概,即使此時Presenter沒有持有View赛不,我們依然可以從業(yè)務(wù)邏輯中接收到Model的更新(比如View并未處于棧頂);BehaviorSubjects始終持有它最后的值罢洲,并在View重新依附后將其重新發(fā)射踢故。

規(guī)則很簡單:使用intent()來“包裝”View層的所有intent,使用subscribeViewState()替代Observable.subscribe().

[站外圖片上傳中...(image-1db2b2-1552228859520)]

8.UnbindIntents

bindIntent()相對應(yīng)的是 unbindIntents() ,該方法只會執(zhí)行一次,即View被永久銷毀時才會被調(diào)用惹苗。舉個例子殿较,將一個Fragment放在棧中,直到Activity被銷毀之前桩蓉,該View一直不會被銷毀淋纲。

由于intent()subscribeViewState()已經(jīng)對訂閱進行了管理,因此您只需要實現(xiàn)unbindIntents()院究。

9.其它生命周期的事件

那么其它生命周期的事件洽瞬,比如onPause()onResume()又該如何處理?我依然認為Presenter不需要生命周期的事件业汰,然而伙窃,如果你堅持認為你需要將這些生命周期的事件視為另一種形式的intent,您的View可以提供一個pauseIntent()样漆,它是由android生命周期觸發(fā)为障,而又不是按鈕點擊事件這樣的由用戶交互觸發(fā)的intent——但兩者都是有效的意圖。

結(jié)語

第二小節(jié)中放祟,我們探討了Model-View-Intent的基礎(chǔ)鳍怨,并通過MVI淺嘗輒止實現(xiàn)了一個簡單的頁面。也許這個例子太簡單了跪妥,所以你尚未感受到MVI模式的優(yōu)點:代表 狀態(tài)Model和與傳統(tǒng)MVP或者MVVM相比的 單項數(shù)據(jù)流京景。

MVPMVVM并沒有什么問題,我也并非是在說MVI比其它架構(gòu)模式更優(yōu)秀骗奖,但是,我認為MVI可以幫助我們 為復(fù)雜的問題編寫優(yōu)雅的代碼 ,這也正如我們將在本系列博客的 下一小節(jié)(第3小節(jié))中探討的那樣——屆時我們將針對 狀態(tài)合并 (state reducers)的問題進行探討执桌,歡迎關(guān)注鄙皇。


系列目錄

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

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

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


關(guān)于我

Hello,我是卻把清梅嗅仰挣,如果您覺得文章對您有價值伴逸,歡迎 ??,也歡迎關(guān)注我的博客或者Github膘壶。

如果您覺得文章還差了那么點東西错蝴,也請通過關(guān)注督促我寫出更好的文章——萬一哪天我進步了呢?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颓芭,一起剝皮案震驚了整個濱河市顷锰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌亡问,老刑警劉巖官紫,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異州藕,居然都是意外死亡束世,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門床玻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毁涉,“玉大人,你說我怎么就攤上這事锈死∑堆撸” “怎么了?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵馅精,是天一觀的道長严嗜。 經(jīng)常有香客問我,道長洲敢,這世上最難降的妖魔是什么漫玄? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮压彭,結(jié)果婚禮上睦优,老公的妹妹穿的比我還像新娘。我一直安慰自己壮不,他們只是感情好汗盘,可當(dāng)我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著询一,像睡著了一般隐孽。 火紅的嫁衣襯著肌膚如雪癌椿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天菱阵,我揣著相機與錄音踢俄,去河邊找鬼。 笑死晴及,一個胖子當(dāng)著我的面吹牛都办,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播虑稼,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼琳钉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蛛倦?” 一聲冷哼從身側(cè)響起歌懒,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胰蝠,沒想到半個月后歼培,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡茸塞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年躲庄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钾虐。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡噪窘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出效扫,到底是詐尸還是另有隱情倔监,我是刑警寧澤,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布菌仁,位于F島的核電站浩习,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏济丘。R本人自食惡果不足惜谱秽,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望摹迷。 院中可真熱鬧疟赊,春花似錦、人聲如沸峡碉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鲫寄。三九已至吉执,卻和暖如春疯淫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背戳玫。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工峡竣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人量九。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像颂碧,于是被迫代替她去往敵國和親荠列。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,566評論 2 349

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