一個Android項目搞定所有主流架構-2.MVP+單元測試

項目啟發來自谷歌的同類框架項目 android-architecture
利用一個相同的項目,使用不同的框架實現之

這里我找了個自己練手的App作為基礎項目,然后嘗試不同框架實現它
Github地址如下,其中有詳細完全的介紹文檔:
https://github.com/boredream/DesignResCollection

已開發完成的示例


為什么選擇MVP?

相信大部分人都聽過這個框架,或者已經使用過。
了解和簡單運用的過程中大家一定會有這樣幾個問題或者痛點:

  • MVP有什么好處,為什么要用它?

  • MVP結構代碼怎么寫?

  • 為什么MVP結構利于單元測試?而且我為什么要寫測試代碼呢?

  • 好了你說服我了,但是我不會寫單元測試?。?/h4>

  • MVP多了好多類,還要寫測試代碼,寫起來好累??!老娘不想這么麻煩??!

這里班門弄斧的分享下我的經驗,挨個解決這幾個問題。


MVP有什么好處,為什么要用它?

網上文章一大堆,總結下來主要有下面幾個優點:

  • 代碼解耦、結構更清晰
  • 更好的拓展性
  • 可復用性
  • 利于單元測試

優點其實主要是相對傳統MVC結構而言的,簡單對比下:

  • MVC(Model-View-Controller)
    傳統MVC結構中,C承擔著一個總控制器的作用,處理Model數據,再控制View的顯示。
    大部分時候Activity類就是這個角色,我們在Activity中調用接口,接口返回數據后各種setText setImage顯示到UI上。
  • MVP(Model-View-Presenter)
    重點在于Presenter,它其實是將Model和View分開了,在其中起到一個中轉站的角色。
    把Model數據拿來一通處理,然后丟給View讓它自己去解決具體的UI顯示。

打個比方
如果處理Model處理業務邏輯就是加工食材做菜。把菜送到客戶手里呈現給客戶就是View的展示。
那MVC就是大排檔。C就是獨自運營的老板,自己炒菜,做完再自己送到小桌子上的客戶面前,一條龍。
MVP就是正規大餐廳,P則是后廚中心,海綿寶寶做好蟹黃堡后放到窗口處,叮一下通知前臺好了可以送餐了,不用關心菜是怎么送到客戶手里的。然后由服務員章魚哥在窗口處取了餐,再或跑或跳或踩著轱轆鞋最后送到客戶手里,合作完成。

所以這里也可以看出來,MVP最重要的特點就是:

** 將 Model業務邏輯處理 和 View頁面處理 分開?。?! **

MVP的良好拓展性、解耦、利于單元測試等優點基本都是來源于此。

純語言描述大家可能還是不好理解,下面上實戰項目。


MVP結構代碼怎么寫?

示例項目中的MVP結構參考了谷歌官方MVP示例項目中的寫法。每個功能模塊都包含以下幾部分:

  • Contract協議類

這個Contract協議類不是MVP中的任何一個模塊,是把所有View和Presenter的方法都提取成了接口放在這里,作為一個總的規則、協議,方便統一管理。
比如下面的代碼,就是示例項目中意見反饋頁面的Contract協議類,提供了View和Presenter的接口。
其中BaseView和BasePresenter是提供了一些基礎方法,比如顯示進度showProgress等,自己可以按需添加。

public interface FeedBackContract {    
    interface View extends BaseView<Presenter> {        
        void addFeedbackSuccess();    
    }    

    interface Presenter extends BasePresenter {        
        void addFeedback(String content, String email);    
    }
}
  • Model

數據層,和MVC結構中的無區別,沒啥好說的。

  • Presenter

負責處理業務邏輯代碼,處理Model數據,然后分發給View層的抽象接口。
注意,這里是將處理好的數據派發給View的抽象接口,是一個簡單的中轉分發出去,并不負責具體展示

public class FeedBackPresenter implements FeedBackContract.Presenter {
    private final FeedBackContract.View view;
    private final HttpRequest.ApiService api;

    public FeedBackPresenter(FeedBackContract.View view, HttpRequest.ApiService api) {
        this.view = view;
        this.api = api;
        this.view.setPresenter(this);
    }

    @Override
    public void addFeedback(String content, String email) {
        // 開始驗證輸入內容
        if (StringUtils.isEmpty(content)) {
            view.showTip("反饋內容不能為空");
            return;
        }
        if (StringUtils.isEmpty(email)) {
            view.showTip("請輸入郵箱地址,方便我們對您的意見進行及時回復");
            return;
        }

        view.showProgress();

        // 使用自定義對象存至云平臺,作為簡易版的反饋意見收集
        FeedBack fb = new FeedBack();
        fb.setContent(content);
        fb.setEmail(email);
        Observable<BaseEntity> observable = ObservableDecorator.decorate(api.addFeedBack(fb));
        observable.subscribe(new Subscriber<BaseEntity>() {
            @Override
            public void onCompleted() {
            }
            @Override
            public void onError(Throwable e) {
                if (!view.isActive()) {
                    return;
                }

                view.dismissProgress();
                view.showTip("反饋提交失敗");
            }
            @Override
            public void onNext(BaseEntity entity) {
                if (!view.isActive()) {
                    return;
                }

                view.dismissProgress();
                view.addFeedbackSuccess();
            }
        });
    }
}
  • View

負責UI具體實現展現。比如Presenter派發過來一個動作是showProgress顯示進度命令,那由我這個View負責實現具體UI,是顯示進度框還是顯示一個下拉刷新圈圈等,都是View這里自行控制。
Google的例子中,每個Activity中都會添加一個Fragment作為View實現,Activity僅僅作為一個容器,包含一個Fragment在其中顯示各種控件。我覺得其實也可以直接將Activity作為View。本示例代碼中兩種方式都有,可以根據需要自行選擇方式~

public class FeedBackActivity extends BaseActivity implements FeedBackContract.View {
    private FeedBackContract.Presenter presenter;
    private EditText et_content;
    private EditText et_email;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_feed_back);
        initView();
    }

    private void initView() {
        presenter = new FeedBackPresenter(this, HttpRequest.getInstance().service);
        initBackTitle("意見反饋")
                .setRightText("提交")
                .setRightOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        submit();
                    }
                });
        et_content = (EditText) findViewById(R.id.et_content);
        et_email = (EditText) findViewById(R.id.et_email);
    }

    private void submit() {
        // 開始驗證輸入內容
        String content = et_content.getText().toString().trim();
        String email = et_email.getText().toString().trim();
        presenter.addFeedback(content, email);
    }

    @Override
    public void addFeedbackSuccess() {
        showToast("反饋成功");
        finish();
    }

    @Override
    public void setPresenter(FeedBackContract.Presenter presenter) {
        this.presenter = presenter;
    }

    @Override
    public boolean isActive() {
        return isActive;
    }

    @Override
    public void showProgress() {
        showProgressDialog();
    }

    @Override
    public void dismissProgress() {
        dismissProgressDialog();
    }

    @Override
    public void showTip(String message) {
        showToast(message);
    }
}

注意,這里BaseView中會有一個isActivite方法,用于判斷視圖是否被銷毀。我在BaseActivity中會統一處理,添加一個isActivite變量,onStart時設為true,onStop時設為false。
然后在presenter里的接口返回數據后,判斷view是否被銷毀然后再控制顯示,因為接口是異步的,所以返回數據后視圖可能已經銷毀,那就沒必要更新了,更新反而還會崩潰報錯。

** 好了,現在再回頭看看MVP的幾個優點,可能就有更好的理解了(當然,還是要自己擼過一遍最好)。**

  1. 更好的拓展性。
* 某天頁面需要加功能了,協議類中先寫好對應的P邏輯方法、V頁面方法,然后在實現類中分別編寫具體代碼即可。
* 某天突然改功能了,說所有錯誤提示我們不用Toast,用Dialog吧,那直接在showTip處修改即可。
* 某天產品突然告訴你說意見反饋,失敗我們也讓用戶覺得成功,那直接在Error回調里調用view抽象方法即可。
  1. 解耦、更好的代碼結構。
* 業務邏輯 和 頁面UI 代碼分開,不揉在一起,改邏輯的時候不用關心UI,反之亦然。
* 想了解某個模塊功能時,直接在協議類中看一個個抽象方法,不用關心代碼,清晰明了。
* 還有代碼可以分工合作,核心業務邏輯你在P中自己寫,UI的具體實現直接給其他人合作寫。
  1. 可復用性。
    比如本項目中的注冊功能,注冊步驟1和步驟2頁面中都有發送驗證碼功能,那就可以使用同一個P了,在其中調用獲取驗證碼接口。然后各自實現具體View顯示,步驟1頁面獲取驗證碼成功后跳轉到頁面2,頁面2獲取成功后開始數字倒計時。

為什么MVP結構利于單元測試?

之前提到過,MVP結構最大的特點是,P將邏輯和UI分開了。即P 中沒有任何Android相關的代碼,比如Toast啊、setText等等。這意味著~ 你可以針對Presenter寫junit測試了。只對java代碼的測試,不用涉及任何UI!!!不用運行模擬器的測試!!!?。∷俣绕痫w的測試!?。。。。。。?/p>

說的這么熱鬧,那么

我為什么要寫測試代碼呢?不是浪費時間嗎?

測試其實除了檢測bug驗證邏輯之外,還有最重要的一個功能是提高開發速度!
你沒有看錯,雖然寫了更多的代碼,但實際效率是提升的,尤其對越龐大越復雜的應用來說。
可能我這樣說不夠權威,可以看下經典書籍《重構》然后自己嘗試一下,可能就會有感受了。


怎么寫測試代碼呢?

我們先介紹下Android中的兩種測試

  • UI測試(本項目中使用框架Espresso)

UI測試其實就是模擬機器上的操作行為,讓它自動進行的“點擊某個位置”、“輸入某些字符串”等行為。
是依賴安卓設備的,測試的時候可以在手機或模擬器屏幕上看到頁面被各種點點點,輸輸輸,跳來跳去。

這個其實和MVP結構關系不大,MVC,MVP,或者MVABCDEFG都可以進行UI測試,所以這里暫時不多做介紹,可以直接參考示例項目中的代碼。UI測試部分的內容其實也很多,以后單獨拿出來再詳細展開。

項目中androidTest文件夾里的就是UI測試代碼,而test文件夾才是Junit部分的單元測試代碼。

  • 對Presenter進行Junit單元測試(本項目中使用框架Mockito)

UI測試雖然接近真實場景,但是有個缺點是要運行應用到模擬器上,所以速度就會有影響,慢~
而且開發中也會常有這樣一個需要,調試接口時,我不想點點點跳轉到那個頁面再輸入東西再點按鈕,費時間啊~而用postman啥的工具也麻煩,header還要重新寫,如果有參數加密就更蛋疼了。

所以,這個時候你就需要Junit單元測試了,最大的特點就是不用運行安卓設備,直接run代碼,速度飛快!

  • 單元測試代碼示例

正式開始介紹怎么寫之前,先感受下單元測試是什么樣的,如下圖

意見反饋Presenter代碼截圖

這里針對示例項目中意見反饋Presenter分別測試了幾個場景

  • 真實接口提交成功
  • 模擬接口提交成功
  • 模擬接口提交失敗

三個Test方法,針對三個測試場景。
突破左下角運行情況可以看到,一共用了852ms,1秒不到?。?!
第一個測試方法因為是真實調用接口數據,所以稍微耗費點時間。
右下角也可以看到3個用例全測試成功通過,也打印了真實調用數據的接口日志。
完美~


如何寫單元測試代碼

編寫步驟按照以下進行

1. 新建Presenter的測試類
右鍵Presenter類 -> Go To -> Test -> create new test


彈出一個創建測試類對話框,然后勾選需要測試的方法(當然也可以自己手動創建方法)。
然后OK,選擇test文件夾完成測試類創建。

2. 測試類的初始化
代碼如下(mockito的gradle配置等參考項目中build.gradle)

// 用于測試真實接口返回數據
private FeedBackPresenter presenter;

// 用于測試模擬接口返回數據
private FeedBackPresenter mockPresenter;

@Mock
private FeedBackContract.View view;
@Mock
private HttpRequest.ApiService api;

@Before
public void setupMocksAndView() {
    // 使用Mock標簽等需要先init初始化一下
    MockitoAnnotations.initMocks(this);

    // 當view調用isActive方法時,就返回true表示UI已激活。方便測試接口返回數據后測試view的方法
    when(view.isActive()).thenReturn(true);

    // 設置單元測試標識
    BoreConstants.isUnitTest = true;

    // 用真實接口創建反饋
    Presenter presenter = new FeedBackPresenter(view, HttpRequest.getInstance().service);
    // 用mock模擬接口創建反饋
    Presenter mockPresenter = new FeedBackPresenter(view, api);
}

這里用到了一個很重要的框架 Mockito。

Mockito框架介紹

  • Mockito框架是干什么的?

mockito框架是用來模擬數據和情景的,方便我們的測試工作進行。

  • 為什么要用Mockito框架?

比如我們MVP結構中P的測試,有個問題是:創建Presenter對象的時候這個View怎么辦?傳入null會空指針啊。還有很多接口調用等邏輯,很多奇怪的失敗情況怎么測試?
這個時候就可以用mockito了~ 直接模擬一個view接口對象,不用關心它的具體實現;失敗情況直接用when方法搞定;此外還提供了其他一系列方便測試的方法,比如verify用于判斷某對象是否執行了某個方法等。后面會根據例子挨個介紹。

網上很多例子其實是純mock模擬測試,也就是接口api也是模擬的,模擬接口調用,模擬接口返回數據。
雖然這樣速度快且方便模擬各種錯誤情況,但是有時候也會想要測試真實的接口返回情況,因此本項目示例中提供了兩種模擬和真實接口的寫法和處理。參考上面代碼里的presenter和mockPresenter對象。

注意,mock相關方法比如verify、when等使用者也都必須是mock對象,所以使用presenter的時候不能用when什么的方法模擬接口返回。

@Before標簽的方法,是每個測試方法調用前都會走一遍的方法,因此在里面放了一系列的初始化操作,每個操作都添加了注釋。其中需要單獨解釋的是when方法。

when(view.isActive()).thenReturn(true);

這個是mockito框架提供的一個方法,看英文基本就能了解什么意思了,當xx方法調用時就返回xx
因為我們的view的模擬的,所以沒有實現isActive方法,則p中數據返回后就無法繼續走下去了,因此這里when處理一下。只要調用這個方法就返回true。

** 3. 測試方法編寫 **
通常Presenter中的一個業務方法會對應至少一個測試方法。
比如這里的意見反饋業務,就分別對應意見提交成功、失敗兩種情景。
方法名字可以隨便定,有個@Test標簽即可,推薦方法取名為:test+待測方法原名+測試場景
測試場景一共有哪些呢?這個最好問測試要個測試用例按照待測功能對應的所有情景挨個來。

我這里寫的單元測試代碼,對于接口又分了兩種:** 模擬接口 ** 和 ** 真實接口 **

直接全部用真實接口測不很好嗎,為什么要mock模擬測試呢?
好吧,比如我們這個意見反饋,不像登錄還有密碼錯誤的情況,很少有場景能失敗。怎么辦?
所以對于難以模擬的情景,還是需要用mockito框架模擬的,模擬個失敗,然后驗證失敗后的一系列邏輯~

下面挨個介紹測試方法,模擬成功和失敗差不多就只介紹失敗了。

  • 模擬接口測試方法示例 - 模擬提交失敗
@Test
public void testAddFeedback_Mock_Error() throws Exception {
    // 模擬數據,當api調用addFeedBack接口傳入任意值時,就拋出錯誤error
    when(api.addFeedBack(any(FeedBack.class)))
        .thenReturn(Observable.<BaseEntity>error(new Exception("孫賊你說誰辣雞呢?")));

    String content = "這個App真是辣雞!";
    String email = "120@qq.com";
    mockPresenter.addFeedback(content, email);

    verify(view).showProgress();
    verify(view).dismissProgress();
    verify(view).showTip("反饋提交失敗");
}

這里重點是when的運用,當模擬的api調用addFeedBack時,就返回error結果。
然后調用mockPresenter的意見反饋業務方法,最后驗證結果。
注意,這個verify方法也是特別常用的一個mockito方法,用于驗證某個對象是否執行了某個方法。
最后運行測試,成功,完美~

  • 真實接口測試方法示例 - 提交成功
@Test
public void testAddFeedback_Success() throws Exception {
    // 真實數據,調用實際接口
    String content = "這個App真是好!";
    String email = "110@qq.com";
    presenter.addFeedback(content, email);

    verify(view).showProgress();
    verify(view).dismissProgress();
    verify(view).addFeedbackSuccess();
}

這里用了真實接口對應的presenter對象,調用接口,然后驗證成功結果。
運行測試,成功,完美~

再次強調,mockito的方法都是針對模擬對象的,所以調用真實請求api時,你也想用when去處理,那就會報錯~

注意,真實接口由于是異步的,所以如果不做任何處理是無法測試通過的,接口數據還沒返回就運行下面的驗證了,自然失敗。因此需要對回調做一個處理,將其修改為同步請求,這樣就能一條線下來了,運行完接口再進行驗證。項目是基于Retrofit框架的,使用RxJava處理回調,我這里所有的回調都會用一個ObservableDecorator處理一下,而在其中我會判斷,如果當前是測試狀態(也就是Before中的那個isUnitTest 參數),就將回調設置為同步,具體代碼參考項目中。

** 4. 運行單元測試用例 **

  • 右鍵方法,run 測試單個用例方法
  • 右鍵類,run 測試該類中包含的全部用例方法

最后控制臺看結果
參考最上面單元測試代碼示例中的截圖,下面控制臺會顯示測試了哪些方法,測試成功通過了幾個方法,然后打印相應日志,如果不通過還會打印對應錯誤信息。

好了,寫法介紹完畢~
更多例子請去項目中查看,這里篇幅有限就不太詳細的展開了,簡單列舉幾個例子讓大家感受下。


MVP多了好多類,還要寫測試代碼,寫起來好累??!老娘不想這么麻煩啊!

這一點估計是最重要的原因把絕大部分人阻擋在門外。
畢竟平常普通的擼就那么累了,還要這么麻煩,沒時間啊沒精力?。。?!

  • 不一定所有功能都用MVP

就像之前例子舉得那樣,大排檔和正規餐廳。你在一個超級偏遠沒人流量生意差到爆的地方還整個后廚中心,就過了。同理,如果你有的功能業務邏輯比較簡單,自然就沒必要MVP了,簡單的關于頁面你也一頓MVP可能就有點猛了,所以不一定所有功能都使用MVP。

  • 單元測試利于開發

代碼結構啥的就不說了,單元測試這個有時候真的很方便,尤其是運行快。相信大部分人都有經驗,遇到個不靠譜后臺的時候,經常要陪他們調接口,再遇到那種特別深的頁面簡直是浪費人生。單元測試代碼,run,唰~秒搞定。自測某些邏輯功能時也很有用,這一點上看來絕對是節省時間的。

  • LiveTemplate(干貨!?。∫绘I生成模板代碼,模板可自定義!?。。?/h4>

我通常擼的時候特別特別注重速度效率。之前也開發過很多插件工作,比如已經發布的自動生成代碼布局的開源AndroidStudio插件。https://github.com/boredream/BorePlugin

然后就尋思,寫這種特別有規律的MVP各種類,還有測試類等的時候,要不也弄個插件生成下?
但是想了下覺得插件生成模板代碼的話,模板怎么寫呢?尤其MVP這種不同的人寫法也不同啊。
最后突然想起來了AndroidStudio里自帶的LiveTemplate這東西,是AS中自帶的一個模板代碼系統。

使用LiveTemplate模板

先展示下該功能的強大,這里我以前提前寫好過幾個模板了。拿協議類舉例。

  1. 右鍵需要生成的位置 -> New -> 選擇模板(如下圖的MvpContract)


  2. 然后彈出對話框,為模板輸入需要的變量,OK生成


  3. 這樣就按照我們的模板創建了一個文件,右側文件代碼全部都是自動生成的,然后按需修改加入方法即可。


那么模板哪里來的呢~下面介紹

編輯/創建LiveTemplate模板

  1. 編輯已有模板: New -> 選擇模板的時候,模板底部有個Edit File Template,點擊之。參見上面使用步驟1的圖。
  2. 創建新的模板:打開你希望生成模板的文件,選擇工具欄中的Tool -> Save File as Template
  3. 步驟1、 2都會打開下面這樣一個編輯頁面,區別在于創建比編輯少個左側的已有模板列表
    給模板起個名字,然后在內容頁面里根據需要刪刪改改即可,模板里所有${NAME}的地方都會替換成你創建模板時候輸入的文件名,其他的${XXX}的作用可參考下面Description里的描述。最后OK保存模板。


LiveTemplate雖然無法替你搞定絕大部分代碼,但是這樣一個快捷的模板,可以靈活的隨時編輯還是很方便的,還是能節省相當代碼量的。

和本期主題無關的插個話,LiveTemplate是個很神奇的東西,很多地方都可以用,不光有文件的模板,代碼也是。比如輸入sout+回車就會自動生成System.out.print()代碼,輸入Toast+回車就會自動生成Toast.make blablabl的代碼,超級方便。比如你們項目有BaseActivity,需要復寫幾個方法,那就可以自定義創建個頁面類文件模板里面處理好繼承和方法,就不用每次新建完Activity都去寫一下繼承了。更多用法期待你滴挖掘~


結語

好了,之前提的所有問題和痛點都挨個解答過了,尤其最后的LiveTemplate,對于還不知道的同學,即使最后你還是不愿意用MVP和寫單元測試,那這部分你也算賺到了哈哈。

因為要介紹的內容比較多,MVP啊測試啊Junit單元測試啊LiveTemplate啊 所以介紹的比較精簡,主旨在拋磚引玉,希望大家對這幾個東西能有個了解,感興趣后再深入研究,也希望與我多多交流大家共同進步。

本項目里Junit測試模塊其實還是有幾個問題的,比如Presenter我是將接口Api作為構造函數參數依賴注入的,所以其實還可以再加入Dagger2改進一番,下一個框架就會在MVP的結構上加入Dagger2。

谷歌例子中RxJava是單獨拎出來說的,我這里Retrofit2+RxJava是作為所有例子通用框架的,用法可以給大家作為一個參考,這里就不掃盲Retrofit用法了。

最后,如果文章對你有一點作用和啟發,希望能支持一下,歡迎follow我和star本項目
https://github.com/boredream/DesignResCollection

順便打個小廣告,之前提到的自動生成布局代碼插件
https://github.com/boredream/BorePlugin

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容