GOOGLE TODO-MVP 學(xué)習(xí)筆記

GOOGLE TODO-MVP 學(xué)習(xí)筆記

背景(可忽略):《GOOGLE TODO-MVP 學(xué)習(xí)筆記》這篇文章主要會記錄自己在根據(jù)TODO-MVP這個項目學(xué)習(xí)MVP的過程中的一些心得和想法难裆,一是為了自己記錄下來命锄,二是為了說出來脐恩,增強自己的理解驶冒。
由于時間及經(jīng)驗有限骗污,文中可能存在錯誤與不足需忿,歡迎大家指出屋厘,我會第一時間對文章進行修改糾正。

如果對MVP模式不是很了解的溢谤,可以先去看看相關(guān)文章世杀,這里推薦diygreen的兩篇文章瞻坝,MVP詳解上下。
google 項目地址:https://github.com/googlesamples/android-architecture/
選擇不同的分支包晰,本文的是TODO-MVP湿镀,也是最基礎(chǔ)的沥阱。

本文主要講了兩個部分

  1. 在TODO-MVP中是如何實現(xiàn)MVP的
  2. 一個簡單的單元測試

TODO-MVP

先來一個整體的概覽:

整個項目結(jié)構(gòu)特別清晰赋咽,最外層是五個文件夾般眉,兩個代碼目錄,三個測試目錄蒸矛,之前看文章有說四個測試目錄的瀑罗,不過個人不是很認同。其中在main文件夾下是我們主要的代碼(找不到的請切換到Project結(jié)構(gòu))雏掠,展開的部分就是斩祭,可以看到是按照業(yè)務(wù)模塊劃分的,從上到下依次是添加模塊乡话,數(shù)據(jù)層摧玫,統(tǒng)計模塊詳細模塊绑青,展示模塊诬像,工具類PV基類闸婴,名字起的有些隨意坏挠,再看每一個包中的具體類,以tasks為例:

  • ScrollChildSwipeRefreshLayout----自定義View
  • TasksActivity-----------------------------負責(zé)創(chuàng)建V,P
  • TasksContract---------------------------接口邪乍,V,P接口的紐帶
  • TasksFilterType-------------------------枚舉類
  • TasksFragment-------------------------View層實現(xiàn)類
  • TasksPresenter-------------------------Presenter層實現(xiàn)類

先以代碼的方式了解下View層和Presenter層是如果創(chuàng)建并工作的降狠,先來看看Activity:


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

        //UI相關(guān)初始化,忽略
        //通過工具類創(chuàng)建一個Fragment庇楞,是View層的實現(xiàn)類
        TasksFragment tasksFragment =
                (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
        if (tasksFragment == null) {
            // Create the fragment
            tasksFragment = TasksFragment.newInstance();
            ActivityUtils.addFragmentToActivity(
                    getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
        }

        // Create the presenter
        // 創(chuàng)建一個Presenter
        mTasksPresenter = new TasksPresenter(
                Injection.provideTasksRepository(getApplicationContext()), tasksFragment);

        // Load previously saved state, if available.
        //恢復(fù)界面中Task的類別
        if (savedInstanceState != null) {
            TasksFilterType currentFiltering =
                    (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
            mTasksPresenter.setFiltering(currentFiltering);
        }
    }

可以看到在Activity初始化的時候分別創(chuàng)建了一個Fragment(View的實現(xiàn)類)榜配,一個TasksPresenter(Presenter的實現(xiàn)類),注意Presenter在構(gòu)建的時候需要傳入一個View對象姐刁。接下來看看Presenter初始化的時候都做了些什么:


    <-- Injection -- >
    public class Injection {
        public static TasksRepository provideTasksRepository(@NonNull Context context) {
            checkNotNull(context);
            return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                    TasksLocalDataSource.getInstance(context));
        }
    }
    <-- Taskspresenter -- >
     private final TasksRepository mTasksRepository;

    private final TasksContract.View mTasksView;

     public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {
        mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
        mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
        //為View層設(shè)置對應(yīng)的Presenter層對象
        mTasksView.setPresenter(this);
    }

先通過Injection的靜態(tài)方法provideTasksRepository()創(chuàng)建一個TasksRepository(Model層的實現(xiàn)類)芥牌,之后將其與Fragment通過構(gòu)造函數(shù)傳遞到Presenter中烦味,這樣在P層初始化的時候就持有了M和V的對象聂使。之后會通過View.setPresenter(P)方法為View層設(shè)置對應(yīng)的Presenter∶恚看一下Fragment中的代碼:


    <-- BaseView -- >
    public interface BaseView<T> {
        void setPresenter(T presenter);
    }
    <-- TasksFragment -- >

    private TasksContract.Presenter mPresenter;
    @Override
    public void setPresenter(@NonNull TasksContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }

先在View的基類中聲明抽象的設(shè)置方法柏靶,然后在Presenter初始化的時候?qū)resenter注入到View中。

總結(jié)一下:

  1. 在Activity創(chuàng)建的時候創(chuàng)建一個View對象溃论,一個Presenter對象屎蜓。
  2. 在創(chuàng)建presenter的時候?qū)⒁粋€Model,上一步中創(chuàng)建好的View钥勋,通過構(gòu)造函數(shù)注入到Presenter中炬转。
  3. 在Presenter的構(gòu)造方法中辆苔,通過View.setPresenter(P)方法,將Presenter設(shè)置到View中扼劈。

接下來用一個簡單的例子來走一遍整體流程驻啤,以添加一個loadTasks為例:

第一步: 在TasksFragment的onResume()方法中,Presenter層開始工作荐吵。

    <-- TasksFragment -->
    @Override
    public void onResume() {
        super.onResume();
        mPresenter.start();
    }

第二步: TasksPresenter.start()方法中調(diào)用了loadTasks()方法骑冗,我們需要在TasksContract.Presenter中去規(guī)定這個方法,然后再在TasksPresenter中去實現(xiàn)它先煎。


    <-- TasksPresenter -->
    @Override
    public void start() {
        loadTasks(false);
    }

    <-- TasksContract.Presenter -->
     void loadTasks(boolean forceUpdate);

    <-- TasksPresenter -->
    private boolean mFirstLoad = true;
     @Override
    public void loadTasks(boolean forceUpdate) {
        // Simplification for sample: a network reload will be forced on first load.
        loadTasks(forceUpdate || mFirstLoad, true);
        mFirstLoad = false;
    }

    private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
        if (showLoadingUI) {
            mTasksView.setLoadingIndicator(true);
        }
        if (forceUpdate) {
            mTasksRepository.refreshTasks();
        }

        // The network request might be handled in a different thread so make sure Espresso knows
        // that the app is busy until the response is handled.
        EspressoIdlingResource.increment(); // App is busy until further notice
        //調(diào)用TasksRepository.getTasks方法去獲取數(shù)據(jù)
        mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                List<Task> tasksToShow = new ArrayList<Task>();

                // This callback may be called twice, once for the cache and once for loading
                // the data from the server API, so we check before decrementing, otherwise
                // it throws "Counter has been corrupted!" exception.
                if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
                    EspressoIdlingResource.decrement(); // Set app as idle.
                }
                // We filter the tasks based on the requestType
                //篩選想要類型的Task
                for (Task task : tasks) {
                    switch (mCurrentFiltering) {
                        case ALL_TASKS:
                            tasksToShow.add(task);
                            break;
                        case ACTIVE_TASKS:
                            if (task.isActive()) {
                                tasksToShow.add(task);
                            }
                            break;
                        case COMPLETED_TASKS:
                            if (task.isCompleted()) {
                                tasksToShow.add(task);
                            }
                            break;
                        default:
                            tasksToShow.add(task);
                            break;
                    }
                }
                // The view may not be able to handle UI updates anymore
                if (!mTasksView.isActive()) {
                    return;
                }
                if (showLoadingUI) {
                    mTasksView.setLoadingIndicator(false);
                }

                processTasks(tasksToShow);
            }

            @Override
            public void onDataNotAvailable() {
                // The view may not be able to handle UI updates anymore
                if (!mTasksView.isActive()) {
                    return;
                }
                mTasksView.showLoadingTasksError();
            }
        });
    }

第三步:調(diào)用TasksRepository.getTasks()方法贼涩,所以需要在TasksDataSource中添加getTasks,然后讓TasksRepository去實現(xiàn)這個方法薯蝎,在這個方法中調(diào)用具體的數(shù)據(jù)層的實現(xiàn)類mTasksRemoteDataSource遥倦,mTasksLocalDataSource中的getTasks,之后通過傳遞過來的接口將數(shù)據(jù)返回到Presenter中占锯。

    
    <-- TasksRepository -->
    //具體的遠程數(shù)據(jù)實現(xiàn)類
    private final TasksDataSource mTasksRemoteDataSource;
    //具體的本地數(shù)據(jù)實現(xiàn)類
    private final TasksDataSource mTasksLocalDataSource;
    //內(nèi)存緩存
    Map<String, Task> mCachedTasks;

     @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        checkNotNull(callback);

        // Respond immediately with cache if available and not dirty
        if (mCachedTasks != null && !mCacheIsDirty) {
            callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            return;
        }
        if (mCacheIsDirty) {
            // If the cache is dirty we need to fetch new data from the network.
            getTasksFromRemoteDataSource(callback);
        } else {
            // Query the local storage if available. If not, query the network.
            mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
                @Override
                public void onTasksLoaded(List<Task> tasks) {
                    refreshCache(tasks);
                    callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
                }
                @Override
                public void onDataNotAvailable() {
                    getTasksFromRemoteDataSource(callback);
                }
            });
        }
    }

第四步:調(diào)用mTasksView.showTasks()展示數(shù)據(jù)谊迄,所以需要在TasksContract.View中定義方法 void showTasks(List<Task> tasks);然后在TasksFragment中去實現(xiàn)。


    <-- TasksPresneter -->
     private void processTasks(List<Task> tasks) {
        if (tasks.isEmpty()) {
            // Show a message indicating there are no tasks for that filter type.
            processEmptyTasks();
        } else {
            // Show the list of tasks
            //顯示查詢回來的數(shù)據(jù)
            mTasksView.showTasks(tasks);
            // Set the filter label's text.
            showFilterLabel();
        }
    }
    <-- TasksFragment -->
    @Override
    public void showTasks(List<Task> tasks) {
        mListAdapter.replaceData(tasks);

        mTasksView.setVisibility(View.VISIBLE);
        mNoTasksView.setVisibility(View.GONE);
    }

這樣一個流程就跑通了烟央,從在View中調(diào)用Presenter方法去請求數(shù)據(jù)统诺,Presenter中調(diào)用Model方法去獲取數(shù)據(jù),Model在調(diào)用具體實現(xiàn)方法疑俭,獲取數(shù)據(jù)之后粮呢,將數(shù)據(jù)通過接口返回到Presenter中,之后再調(diào)用View的方法展示數(shù)據(jù)钞艇。
但是啄寡,如果是按照上面的順序去寫代碼的話,肯定會覺得這實在是太復(fù)雜了哩照,多寫好多東西挺物,所以個人猜測應(yīng)該不是按照上面的方式去寫的,猜測應(yīng)該是這樣:

第一步:在TasksContract.Presenter中去寫一個方法讓它去加載數(shù)據(jù)飘弧,比如LoadTasks()识藤;
第二步:在TasksDataSource中寫一個方法讓它去獲取數(shù)據(jù),比如getTasks()次伶,之后再定義一個接口痴昧,用于傳遞數(shù)據(jù),抽象一個方法參數(shù)是Task集合冠王,一個簡單的接口回調(diào)赶撰。
第三步:在TasksContract.View中寫一個方法,用于展示數(shù)據(jù),參數(shù)肯定是要展示的數(shù)據(jù)了豪娜,比如showTasks(tasks)餐胀;
第四步:寫各自的實現(xiàn)方法。在View中不用考慮數(shù)據(jù)是怎么來的瘤载,只管UI的變化就好骂澄;在Presenter中不用管怎么展示,怎么獲取數(shù)據(jù)惕虑,只管應(yīng)該找誰要數(shù)據(jù)坟冲,之后處理一下,交給View去顯示就好了溃蔫;在Model中健提,只需要得到數(shù)據(jù),傳遞給Presenter就可以了伟叛,其他的完全不用操心私痹。

在每一個自己的層級中做自己應(yīng)該做的事情,并且對其他的東西盡量少的了解统刮,盡可能的不出現(xiàn)干涉紊遵,專注做自己的事情,這樣代碼寫起來其實會清晰很多侥蒙,更加富有條理暗膜,而且在以后的擴展或者修改會變得更加的容易,而不會有那種牽一發(fā)而動全身的感覺鞭衩。

不知道各位對上面第二種寫代碼的方法覺得怎么樣学搜,個人認為,當(dāng)接口方法確定了之后论衍,其實整個開發(fā)工作基本上就完成百分之七十了瑞佩,在View中不用去考慮業(yè)務(wù)邏輯,不用去考慮UI的變化坯台,因為數(shù)據(jù)傳遞過來之后所有的事情就都已經(jīng)確定了炬丸,在Presenter中不用去考慮數(shù)據(jù)的來源,在Model中不去考慮數(shù)據(jù)的預(yù)處理和變換蜒蕾,將所有需要做的功能或者是動作都盡可能的細化稠炬,細化到每一層的每一個方法中,在一個方法中只做一件事情滥搭,其他的并不知道酸纲,也不需要知道捣鲸,剩下的工作就是簡單的填充代碼了瑟匆。

一個簡單的單元測試

其實在TODO-MVP中測試的代碼要比正式的代碼要多,雖然沒有具體數(shù)過,不過從目錄數(shù)量來看就已經(jīng)證明了一點愁溜,測試真的很重要疾嗅,我之前從來沒有寫過任何測試代碼,也沒有專門學(xué)過冕象,只是在平時看了幾篇測試相關(guān)的文章代承,太深的講不了,太淺的說著也沒意思渐扮,就拿一個例子來說论悴,當(dāng)然,在說具體的例子之前墓律,如果各位對相關(guān)的單元測試的知識不是很了解的話膀估,推薦大家?guī)灼恼? 。
鄒小創(chuàng)耻讽,相關(guān)測試文章十一篇察纯,由淺入深,通俗易懂针肥。
鍵盤男饼记,介紹一些實際測試中的經(jīng)驗。
單元測試?yán)?Mockito 中文文檔介紹Mockito相關(guān)API和使用方法慰枕,很全面具则。
看過以上大神的文章之后,自己再隨便瀏覽一些相關(guān)文章具帮,基本上就沒問題了乡洼。

接下來就是例子了,代碼是在test文件夾下tasks包中的TasksPresenter類


    <--TasksPresenterTest>
    private static List<Task> TASKS;

    @Mock
    private TasksRepository mTasksRepository;

    @Mock
    private TasksContract.View mTasksView;

    /**
     * {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to
     * perform further actions or assertions on them.
     */
    @Captor
    private ArgumentCaptor<LoadTasksCallback> mLoadTasksCallbackCaptor;

    private TasksPresenter mTasksPresenter;

    @Before
    public void setupTasksPresenter() {
        // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
        // inject the mocks in the test the initMocks method needs to be called.
        MockitoAnnotations.initMocks(this);

        // Get a reference to the class under test
        mTasksPresenter = new TasksPresenter(mTasksRepository, mTasksView);

        // The presenter won't update the view unless it's active.
        when(mTasksView.isActive()).thenReturn(true);

        // We start the tasks to 3, with one active and two completed
        TASKS = Lists.newArrayList(new Task("Title1", "Description1"),
                new Task("Title2", "Description2", true), new Task("Title3", "Description3", true));
    }

如果上面的代碼匕坯,看不明白束昵,那還是去閱讀我剛才推薦的文章,這里就簡單的說一下葛峻,先是mock了兩個對象,相關(guān)的model和view锹雏。通過@Before 注解,在所有的測試方法做初始化术奖,依據(jù)mock的對象礁遵,創(chuàng)建一個presenter,設(shè)置測試樁采记,初始化數(shù)據(jù)佣耐。
接下來只看一個方法:


    <--TasksPresenterTest>
    @Test
    public void loadAllTasksFromRepositoryAndLoadIntoView() {
        // Given an initialized TasksPresenter with initialized tasks
        // When loading of Tasks is requested
        //提供一個篩選的Task類型
        mTasksPresenter.setFiltering(TasksFilterType.ALL_TASKS);
        //加載數(shù)據(jù)
        mTasksPresenter.loadTasks(true);
        // Callback is captured and invoked with stubbed tasks
        //驗證model的加載數(shù)據(jù)的方法是否執(zhí)行,并且對入?yún)⑦M行捕獲
        verify(mTasksRepository).getTasks(mLoadTasksCallbackCaptor.capture());
        //設(shè)置接口回調(diào)傳遞回來的數(shù)據(jù)
        mLoadTasksCallbackCaptor.getValue().onTasksLoaded(TASKS);

        // Then progress indicator is shown
        //驗證mock view的方法執(zhí)行順序唧龄,創(chuàng)建一個inorder對象
        InOrder inOrder = inOrder(mTasksView);
        //驗證view.setLoadingIndicator(true)是否執(zhí)行
        inOrder.verify(mTasksView).setLoadingIndicator(true);
        // Then progress indicator is hidden and all tasks are shown in UI
        //驗證view.setLoadingIndicator(false)是否執(zhí)行
        inOrder.verify(mTasksView).setLoadingIndicator(false);
        //創(chuàng)建一個參數(shù)捕獲器
        ArgumentCaptor<List> showTasksArgumentCaptor = ArgumentCaptor.forClass(List.class);
        //驗證view.showTasks()方法是否執(zhí)行兼砖,并且執(zhí)行對其參數(shù)進行捕獲
        verify(mTasksView).showTasks(showTasksArgumentCaptor.capture());
        //斷言 判斷捕獲的參數(shù)也就是傳入showTasks方法中的list的size是否為3
        assertTrue(showTasksArgumentCaptor.getValue().size() == 3);
    }

代碼中對每一句都進行了注釋,很好理解,這里對參數(shù)捕獲器說一下讽挟,最開始我不明白這個東西是個什么玩意懒叛,怎么工作的,網(wǎng)上查就說是參數(shù)捕獲器耽梅,能夠捕捉到一個方法的入?yún)⒌南嚓P(guān)信息薛窥。然后自己就照著demo寫,寫完發(fā)現(xiàn)一運行報錯了眼姐,如果是驗證方法诅迷, 或者斷音之類的問題,會有相關(guān)提示的众旗,我這報錯沒有啊竟贯。


我還以為是為代碼寫的有問題,就把google里的代碼拷過來逝钥,運行屑那,還是不行,這就奇怪了艘款,為什么同樣的的代碼在別人那就沒問題持际,在我這就報錯那,之后就開始排查問題哗咆,先一句句從下往上注釋掉蜘欲,運行,看看是哪句出的問題晌柬,被我發(fā)現(xiàn)了是這句verify(mTasksView).showTasks(showTasksArgumentCaptor.capture());姥份,這我就更不明白了,這個是展示數(shù)據(jù)的年碘,肯定沒問題的啊澈歉,然后就去原代碼中去排查,從上到下看了一遍屿衅,沒問題埃难,又看了一遍沒問題,然后又回到測試代碼涤久,各種改涡尘,想看看是哪的問題,就上面的那幾句測試代碼响迂,我玩了半天考抄,還是沒有找到為什么,不行了蔗彤,估計是自己對mock這個東西不是很了解川梅,就去查網(wǎng)上的資料疯兼,看各種譯文,實例文章挑势,介紹文章镇防。當(dāng)看到下面內(nèi)容的時候我好想似乎明白了些什么啦鸣。

是不是view.showTasks()方法沒有執(zhí)行啊潮饱,那樣的話參數(shù)就不會被捕獲,所以就去之前正式代碼中添加打印語句诫给,發(fā)現(xiàn)香拉,確實沒有執(zhí)行,為什么沒有執(zhí)行那中狂,繼續(xù)往上找凫碌,在數(shù)據(jù)遍歷的時候:

我擦嘞,我居然把集合寫錯了胃榕,可能是敲的時候沒注意直接就確定了盛险,也沒看是哪一個了,改過來之后再運行勋又,總終于成功了苦掘,就這么個問題,搞了我一天半楔壤,不過經(jīng)歷了這么個事情之后鹤啡,我發(fā)現(xiàn)對于這些基本的測試樁,驗證蹲嚣,斷言递瑰,順序執(zhí)行,熟悉的不要不要的隙畜,真是沒有磨難就沒有進步啊抖部。

這樣一個簡單的單元測試就完成了,測試內(nèi)容那就是presenter加載數(shù)據(jù)议惰,驗證model獲取數(shù)據(jù)您朽,設(shè)置接口回調(diào)參數(shù),驗證view方法執(zhí)行順序换淆,驗證view方法是否執(zhí)行哗总,捕獲參數(shù),比對參數(shù)內(nèi)數(shù)據(jù)倍试。

多說幾句

說一下我在學(xué)習(xí)這個項目的一些心得體會吧:

  1. 代碼不是看的讯屈,一定要敲。不知道大家怎么去學(xué)習(xí)別人的項目县习,在我看來涮母,最好的學(xué)習(xí)方式谆趾,就是把別人的項目敲一遍,看的時候有可能不過腦叛本,敲的時候就肯定得思考了沪蓬,為什么這么分包,應(yīng)該怎么調(diào)用来候,之類的跷叉。
  2. 什么東西都不要浮于表面。因為現(xiàn)在已經(jīng)不是那個营搅,我見過云挟,我了解的時代了。要盡可能的做到转质,我知道园欣,我熟悉,我敲過休蟹,我寫過相關(guān)案例沸枯, 我看過源碼,我了解底層實現(xiàn)赂弓。
  3. 關(guān)于這個項目還有一部分沒有介紹绑榴,那就是UI測試,目前正在看資料拣展,之后應(yīng)該會出一篇彭沼,不過應(yīng)該是在整個項目都敲完的時候了。其實個人感覺备埃,單元測試這個東西姓惑,重在經(jīng)驗,入門其實很容易按脚,五六個注解于毙,三四個方法,打樁辅搬,驗證唯沮,斷言,任何一個人估計半天到一天應(yīng)該都差不多堪遂,感覺更重要的是正式代碼的書寫介蛉,如果正式代碼寫的不好,測試代碼寫都不能寫溶褪,更別說驗證了币旧,而且測試經(jīng)驗很重要,只有經(jīng)歷足夠多的測試案例猿妈,才會真正掌握單元測試的精髓吧吹菱,所謂的測試驅(qū)動開發(fā)巍虫,想想就覺得好激動。
  4. 我這才剛?cè)腴T鳍刷,還差得遠那占遥。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市输瓜,隨后出現(xiàn)的幾起案子瓦胎,更是在濱河造成了極大的恐慌,老刑警劉巖前痘,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凛捏,死亡現(xiàn)場離奇詭異担忧,居然都是意外死亡芹缔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門瓶盛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來最欠,“玉大人,你說我怎么就攤上這事惩猫≈ビ玻” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵轧房,是天一觀的道長拌阴。 經(jīng)常有香客問我,道長奶镶,這世上最難降的妖魔是什么迟赃? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮厂镇,結(jié)果婚禮上纤壁,老公的妹妹穿的比我還像新娘。我一直安慰自己捺信,他們只是感情好酌媒,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著迄靠,像睡著了一般秒咨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掌挚,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天雨席,我揣著相機與錄音,去河邊找鬼疫诽。 笑死舅世,一個胖子當(dāng)著我的面吹牛旦委,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播雏亚,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼缨硝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了罢低?” 一聲冷哼從身側(cè)響起查辩,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎网持,沒想到半個月后宜岛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡功舀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年萍倡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辟汰。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡列敲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出帖汞,到底是詐尸還是另有隱情戴而,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布翩蘸,位于F島的核電站所意,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏催首。R本人自食惡果不足惜扶踊,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望翅帜。 院中可真熱鬧姻檀,春花似錦、人聲如沸涝滴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽歼疮。三九已至杂抽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間韩脏,已是汗流浹背缩麸。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赡矢,地道東北人杭朱。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓阅仔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親弧械。 傳聞我的和親對象是個殘疾皇子八酒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

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