介紹 Android MVVM 模式

對(duì)于 Android 的 Data Binding 庫(kù)我一段時(shí)間以來(lái)一直非常關(guān)注. 我決定使用 Model-View-ViewModel 架構(gòu)實(shí)現(xiàn)來(lái) 試驗(yàn) 它, 為了做這個(gè), 我拿出了和@matto1990 一起做的應(yīng)用 HackerNews Reader , 用 Model-View-ViewModel 架構(gòu)重新實(shí)現(xiàn)了它.

這篇文建引用了一個(gè)演示MVVM 方式實(shí)現(xiàn)的實(shí)例 app . 為了幫助你的理解我建議你看一看這里的示例 APP 的倉(cāng)庫(kù)

What is MVVM?

Model-View-ViewModel 是一個(gè)實(shí)現(xiàn)建構(gòu)用于抽象 view 的狀態(tài)和行為, 可以讓我們分離 UI 和業(yè)務(wù)邏輯的開(kāi)發(fā). 這是通過(guò)引入一個(gè) ViewModel 實(shí)現(xiàn)的, 其職責(zé)是展現(xiàn)一個(gè) model 數(shù)據(jù)對(duì)象并處理應(yīng)用所有包含在 view 展現(xiàn)的邏輯.

這種方法(MVVM)是由三個(gè)核心部分,每個(gè)都有它自己的不同而獨(dú)立的作用:

  • Model - 包含業(yè)務(wù)和驗(yàn)證邏輯的數(shù)據(jù)模型
  • View -- 定義結(jié)構(gòu),布局和View 在屏幕上的顯示
  • ViewModel - 充當(dāng) View 和 ViewModel 之間的連接, 處理所有視圖邏輯.

所以這與我們使用的 MVC 方法有設(shè)么不同呢? MVC 的架構(gòu)如下:

  • View View 位于架構(gòu)的頂部, Controller 其次, Modle 在最下.
  • Controller 同時(shí)感知 ViewModel
  • View 僅感知 Model, 而且在任何時(shí)刻都會(huì)收到 Model 改變的通知.

在 MVVM 中, 架構(gòu)是類似的, 但是有一些明確的差別

  • ControllerView Model 替代, 位于 UI 層之下.
  • ViewModel 展現(xiàn) View 所需的數(shù)據(jù)和指令對(duì)象.
  • View Model 接收 Model 的數(shù)據(jù).

You can see here that the two approaches use a similar architecture, with the addition of a View Model and the way that it introduces a different approach to the communication between components. The architecture introduces two-way communication between its components, whereas MVC is only capable of one-way communication.

In a nutshell, MVVM is a progression of the MVC architecture - using an additonal layer of non-visual components on top of the Model (but below the View) to map data closer to the View components in the architecture. We’ll take more of a look at the nature of MVVM over the next few sections.

The Hacker News reader

As previously mentioned, I took an old project of mine and stripped it back for use with this article. The features of this sample application consist of:

  • Retrieval of Posts
  • Viewing a single Post
  • Viewing comments for a Post
  • Viewing a selected authors Posts

This was done in the hope that it would reduce the codebase, hence making it a little easier to follow and understand how the implementation operates. The screens of the app that we’re working with are as shown below:

The main part of the application that I’m going to be looking at is the listing of Posts, shown on the left. The comments screen works in pretty much the same way, with a few slight differences (which we’ll look at later).

Displaying Posts

Each Post instance is displayed in a recycler view within a card view, as shown on the left.

Using MVVM we will be able to abstract the different layers that make up this card, meaning that each MVVM component will only be dealing with its assigned responsibility. Using these different components introduced with MVVM, working together they are able to construct the Post card instance. So how can we break this up?

Model

Quite simply put, the Model consists of the business logic belonging to a Post. This includes different properties such as the id, name, text etc. The code below shows a reduced version of this class:


public class Post {

    public Long id;
    public String by;
    public Long time;
    public ArrayList<Long> kids;
    public String url;
    public Long score;
    public String title;
    public String text;
    @SerializedName("type")
    public PostType postType;

    public enum PostType {
        @SerializedName("story")
        STORY("story"),
        @SerializedName("ask")
        ASK("ask"),
        @SerializedName("job")
        JOB("job");

        private String string;

        PostType(String string) {
            this.string = string;
        }

        public static PostType fromString(String string) {
            if (string != null) {
                for (PostType postType : PostType.values()) {
                    if (string.equalsIgnoreCase(postType.string)) return postType;
                }
            }
            return null;
        }
    }

    public Post() { }

}

The Post Model, stripped back of Parcelable and other methods for readability

Here you can see that all our Post Model contains is it’s properties, no other logic has been placed in this class - that’ll be dealt with by the other components.

View

Our View is responsible for defining the layout, appearance and structure of its components. The View itself will be (ideally) constructed completely of XML, however if any java code is used then it should not consist of any business logic. The View retrieves its data from a View Model through the use of binding. Then at run time, the UI content is set and can be updated when the View Model properties flag any change notification events.

To begin with, we created a custom adapter to use with our RecyclerView. For this, we needed to make a create a BindingHolder to keep a reference to our Binding.

public static class BindingHolder extends RecyclerView.ViewHolder {
    private ItemPostBinding binding;

public BindingHolder(ItemPostBinding binding) {
        super(binding.cardView);
        this.binding = binding;
    }
}

The onBindViewHolder() method is where the actual binding of the ViewModel and View takes place. We create a new ItemPostBinding (generated from our item_post layout) and set the View Model to a new instance of our PostViewModel class.

ItemPostBinding postBinding = holder.binding;
postBinding.setViewModel(new PostViewModel(mContext,
                             mPosts.get(position), mIsUserPosts));

Other than the standard adapter views, that’s pretty much it! The full PostAdapter class is displayed below:


public class PostAdapter extends RecyclerView.Adapter<PostAdapter.BindingHolder> {
    private List<Post> mPosts;
    private Context mContext;
    private boolean mIsUserPosts;

    public PostAdapter(Context context, boolean isUserPosts) {
        mContext = context;
        mIsUserPosts = isUserPosts;
        mPosts = new ArrayList<>();
    }

    @Override
    public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ItemPostBinding postBinding = DataBindingUtil.inflate(
                LayoutInflater.from(parent.getContext()),
                R.layout.item_post,
                parent,
                false);
        return new BindingHolder(postBinding);
    }

    @Override
    public void onBindViewHolder(BindingHolder holder, int position) {
        ItemPostBinding postBinding = holder.binding;
        postBinding.setViewModel(new PostViewModel(mContext, mPosts.get(position), mIsUserPosts));
    }

    @Override
    public int getItemCount() {
        return mPosts.size();
    }

    public void setItems(List<Post> posts) {
        mPosts = posts;
        notifyDataSetChanged();
    }

    public void addItem(Post post) {
        mPosts.add(post);
        notifyDataSetChanged();
    }

    public static class BindingHolder extends RecyclerView.ViewHolder {
        private ItemPostBinding binding;

        public BindingHolder(ItemPostBinding binding) {
            super(binding.cardView);
            this.binding = binding;
        }
    }

}

Moving on to our XML layout file, we first begin by wrapping our entire layout in a <layout> tag and declare our ViewModel using the <data> tag:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewModel" type="com.hitherejoe.mvvm_hackernews.viewModel.PostViewModel" /></data>
<!-- Other layout views -->
</layout>

Declaring our View Model is required to allow us to reference it throughout our layout file. I’ve made use of the ViewModel in several places within the item_post layout:

  • androidText - It’s possible to set the content of a text view by referencing the corresponding method in our ViewModel. You can see below the use of @{viewModel.postTitle}, this references the getPostTitle() method in our ViewModel - which returns us the title of the corresponding post instance.
  • onClick - We can also reference click events from our layout file. As shown in the layout file, @{viewModel.onClickPost} is used to reference the onClickPost() method in our ViewModel, which returns an OnClickListener containing the click event.
  • visibility - The ability to open the comments activity for a post depends on whether the post has any comments or not. This is done by checking the size of the comments list and setting the visibility based on the result, which should take place in the ViewModel. Here, we use the getCommentsVisiblity() method which returns the calculated visibility.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="viewModel" type="com.hitherejoe.mvvm_hackernews.viewModel.PostViewModel" />
    </data>

    <android.support.v7.widget.CardView
        xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="2dp"
        android:layout_marginBottom="2dp"
        card_view:cardCornerRadius="2dp"
        card_view:cardUseCompatPadding="true">

        <LinearLayout
            android:id="@+id/container_post"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clickable="true"
            android:orientation="vertical"
            android:onClick="@{viewModel.onClickPost}">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="16dp"
                android:background="@drawable/touchable_background_white">

                <TextView
                    android:id="@+id/text_post_title"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="8dp"
                    android:text="@{viewModel.postTitle}"
                    android:textColor="@color/black_87pc"
                    android:textSize="@dimen/text_large_title"
                    android:onClick="@{viewModel.onClickPost}"/>

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <TextView
                        android:id="@+id/text_post_points"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentLeft="true"
                        android:text="@{viewModel.postScore}"
                        android:textSize="@dimen/text_body"
                        android:textColor="@color/hn_orange" />

                    <TextView
                        android:id="@+id/text_post_author"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_toRightOf="@+id/text_post_points"
                        android:text="@{viewModel.postAuthor}"
                        android:textColor="@color/black_87pc"
                        android:textSize="@dimen/text_body"
                        android:bufferType="spannable"
                        android:onClick="@{viewModel.onClickAuthor}"/>

                </RelativeLayout>

            </LinearLayout>

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="@color/light_grey" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:background="@color/white">

                <TextView
                    android:id="@+id/text_view_post"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:padding="16dp"
                    android:background="@drawable/touchable_background_white"
                    android:clickable="true"
                    android:textColor="@color/black"
                    android:textSize="@dimen/text_small_body"
                    android:textStyle="bold"
                    android:text="@string/view_button"
                    android:onClick="@{viewModel.onClickPost}"/>

                <TextView
                    android:id="@+id/text_view_comments"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:padding="16dp"
                    android:background="@drawable/touchable_background_white"
                    android:clickable="true"
                    android:textColor="@color/hn_orange"
                    android:textSize="@dimen/text_small_body"
                    android:text="@string/comments_button"
                    android:onClick="@{viewModel.onClickComments}"
                    android:visibility="@{viewModel.commentsVisibility}"/>

            </LinearLayout>

        </LinearLayout>

    </android.support.v7.widget.CardView>

</layout>

That’s great isn’t it? This allows us to abstract the display logic from our layout file, handing it over to our ViewModel to take care of it for us.

ViewModel

The ViewModel is the component which acts as the link between the View and the Model, giving it the responsibility of all of the logic relating to our View. The ViewModel is responsible for accessing the methods and properties of the Model, which is then made available to the View. Within our ViewModel, this data can be returned as is or formatted to again remove this responsibility from other components.

In our case, the PostViewModel uses the Post object to handle the display of content on the CardView of a Post instance. Within this class (below) you can see a whole bunch of methods, each corresponding to a different property of our Post View.

  • getPostTitle() - This uses the Post instance to return the Post title
  • getPostAuthor() - This method begins by retreiving a String from the app resources and formatting it with the author of the Post instance. Then if our isUserPosts equates to true we underline the text, finally returning our content String
  • getCommentsVisibility() - This method returns the value that should be used for the comment TextViews visibility
  • onClickPost() - This method returns a click event when the corresponding view is pressed

These samples show the different kinds of logic that can currently be handled by our ViewModel. Below shows the complete PostViewModel class and its methods which are referenced from our item_post view.


public class PostViewModel extends BaseObservable {

    private Context context;
    private Post post;
    private Boolean isUserPosts;

    public PostViewModel(Context context, Post post, boolean isUserPosts) {
        this.context = context;
        this.post = post;
        this.isUserPosts = isUserPosts;
    }

    public String getPostScore() {
        return String.valueOf(post.score) + context.getString(R.string.story_points);
    }

    public String getPostTitle() {
        return post.title;
    }

    public Spannable getPostAuthor() {
        String author = context.getString(R.string.text_post_author, post.by);
        SpannableString content = new SpannableString(author);
        int index = author.indexOf(post.by);
        if (!isUserPosts) content.setSpan(new UnderlineSpan(), index, post.by.length() + index, 0);
        return content;
    }

    public int getCommentsVisibility() {
        return  post.postType == Post.PostType.STORY && post.kids == null ? View.GONE : View.VISIBLE;
    }

    public View.OnClickListener onClickPost() {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Post.PostType postType = post.postType;
                if (postType == Post.PostType.JOB || postType == Post.PostType.STORY) {
                    launchStoryActivity();
                } else if (postType == Post.PostType.ASK) {
                    launchCommentsActivity();
                }
            }
        };
    }

    public View.OnClickListener onClickAuthor() {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                context.startActivity(UserActivity.getStartIntent(context, post.by));
            }
        };
    }

    public View.OnClickListener onClickComments() {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                launchCommentsActivity();
            }
        };
    }

    private void launchStoryActivity() {
        context.startActivity(ViewStoryActivity.getStartIntent(context, post));
    }

    private void launchCommentsActivity() {
        context.startActivity(CommentsActivity.getStartIntent(context, post));
    }
}

Great, huh? As you can see, our PostViewModel takes care of:

  • Providing the Post objects properties to be displayed in our view
  • Carrying out any required formatting on these properties
  • Returning click events to any views using the onClick attribute
  • Handling the visibility of any views based on Post properties

Testing the ViewModel

One of the great things about MVVM is that our View Model is now extremely easy to unit test. For the PostViewModel, a simple test class was created to test that the methods in the ViewModel were implemented correctly.

  • shouldGetPostScore() - Test the getPostScore() method, to ensure that the score for the Post is correctly formatted as a String and returned.
  • shouldGetPostTitle() - Test the getPostTitle() method, to ensure that the correct Post title is returned.
  • shouldGetPostAuthor() - Test the getPostAuthor() method, to ensure that a correctly formatted string using the Post author is returned.
  • shouldGetCommentsVisiblity() - Test that the getCommentsVisibility() method returns the correct visibility for the ‘Comments’ button on the Post card. We pass ArrayLists of different states to ensure that the correct visibility is returned for each case.

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = DefaultConfig.EMULATE_SDK, manifest = DefaultConfig.MANIFEST)
public class PostViewModelTest {

    private Context mContext;
    private PostViewModel mPostViewModel;
    private Post mPost;

    @Before
    public void setUp() {
        mContext = RuntimeEnvironment.application;
        mPost = MockModelsUtil.createMockStory();
        mPostViewModel = new PostViewModel(mContext, mPost, false);
    }

    @Test
    public void shouldGetPostScore() throws Exception {
        String postScore = mPost.score + mContext.getResources().getString(R.string.story_points);
        assertEquals(mPostViewModel.getPostScore(), postScore);
    }

    @Test
    public void shouldGetPostTitle() throws Exception {
        assertEquals(mPostViewModel.getPostTitle(), mPost.title);
    }

    @Test
    public void shouldGetPostAuthor() throws Exception {
        String author = mContext.getString(R.string.text_post_author, mPost.by);
        assertEquals(mPostViewModel.getPostAuthor().toString(), author);
    }

    @Test
    public void shouldGetCommentsVisibility() throws Exception {
        // Our mock post is of the type story, so this should return gone
        mPost.kids = null;
        assertEquals(mPostViewModel.getCommentsVisibility(), View.GONE);
        mPost.kids = new ArrayList<>();
        assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE);
        mPost.kids = null;
        mPost.postType = Post.PostType.ASK;
        assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE);
    }
}

And now we know that our ViewModel is working as it should, great!

Comments

The approach used for comments is very similar to that of the Post instances, however there is one difference that I would like to point out.

Two different view models are used regarding the comments, the CommentHeaderViewModel and CommentViewModel. If you look at the CommentAdapter then you’ll notice two different view types, which are:

private static final int VIEW_TYPE_COMMENT = 0;
private static final int VIEW_TYPE_HEADER = 1;

If the Post type is an Ask post, then we show a header section at the top of the screen containing the question which was asked - the comments are displayed as normal below. You’ll notice in the onCreateViewHolder() method we inflate the layout based on the VIEW_TYPE that we are currently dealing with, this simply returns one of our two different layouts.

if (viewType == _VIEW_TYPE_HEADER_) {
    ItemCommentsHeaderBinding commentsHeaderBinding =
    DataBindingUtil._inflate_(
            LayoutInflater._from_(parent.getContext()),
            R.layout._item_comments_header_,
            parent,
            false);
    return new BindingHolder(commentsHeaderBinding);
} else {
    ItemCommentBinding commentBinding =
        DataBindingUtil._inflate_(
            LayoutInflater._from_(parent.getContext()),
            R.layout._item_comment_,
            parent,
            false);
    return new BindingHolder(commentBinding);
}

Then in our onBindViewHolder() method we create the binding depending on the type view that we’re dealing with. This is because we’re using a slightly different View Model for the cases when there is a header section (for our ASK post question text) used.

if (getItemViewType(position) == _VIEW_TYPE_HEADER_) {
    ItemCommentsHeaderBinding commentsHeaderBinding =
                        (ItemCommentsHeaderBinding) holder.binding;
    commentsHeaderBinding.setViewModel(new
                          CommentHeaderViewModel(mContext, mPost));
} else {
    int actualPosition = (postHasText()) ? position - 1 : position;
    ItemCommentBinding commentsBinding =
                               (ItemCommentBinding) holder.binding;
    mComments.get(actualPosition).isTopLevelComment =
                                               actualPosition == 0;
    commentsBinding.setViewModel(new CommentViewModel(
                         mContext, mComments.get(actualPosition)));
}

And that’s pretty much all that is different about it, the comments section just has two different ViewModel types available - the one chosen is dependent on whether the post is an ASK post or not.

To conclude…

The data binding library, if used correctly, has the potential to really change the way in which we develop applications. There are other ways in which we could make use of data binding in our applications, using an MVVM structure is just one of the ways in which we can do so.

For example, we could simply reference our Model in the layout file and access its properties through a variable reference:

<data>
    <variable name="post" type="your.package.name.model.Post"/>
</data>
<TextView
    ...
    android:text="@{post.title}"/>

This is simple to do and could help to remove some basic display logic from adapters and/or classes. Whilst this is nice, a similar approach could result in the following:

<data>
    <import type="android.view.View"/>
</data>
<TextView
    ...
    android:visibility="@{post.hasComments ? View.Visible :
    View.Gone}"/>

Me upon seeing the above.

For me, this is where Data Binding could have a negative effect on its usage. This is moving a Views display logic into the View itself. Not only do I find this messy, but it would also make testing / debugging more difficult by mixing logic and layout code together.

It’s still too early to know if this approach is the correct way of developing an application, but this experiment has given me a chance to look at one of the possibilities for future projects. It’s something I definitely want to play around with more. If you wish to read more about the Data Binding library, you can do so here. Microsoft has also written a short and easy to understand article on MVVM here.

I’d love to hear your thoughts on this experiment, if you’ve got any comments and/or suggestions then feel free to leave a response or drop me a tweet!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末微猖,一起剝皮案震驚了整個(gè)濱河市约谈,隨后出現(xiàn)的幾起案子圣絮,更是在濱河造成了極大的恐慌关贵,老刑警劉巖扯饶,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喳魏,死亡現(xiàn)場(chǎng)離奇詭異棉浸,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)刺彩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門迷郑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)枝恋,“玉大人,你說(shuō)我怎么就攤上這事嗡害》俾担” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵霸妹,是天一觀的道長(zhǎng)十电。 經(jīng)常有香客問(wèn)我,道長(zhǎng)抑堡,這世上最難降的妖魔是什么摆出? 我笑而不...
    開(kāi)封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮首妖,結(jié)果婚禮上偎漫,老公的妹妹穿的比我還像新娘。我一直安慰自己有缆,他們只是感情好象踊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著棚壁,像睡著了一般杯矩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上袖外,一...
    開(kāi)封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天史隆,我揣著相機(jī)與錄音,去河邊找鬼曼验。 笑死泌射,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鬓照。 我是一名探鬼主播熔酷,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼豺裆!你這毒婦竟也來(lái)了拒秘?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤臭猜,失蹤者是張志新(化名)和其女友劉穎躺酒,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蔑歌,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡羹应,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丐膝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片量愧。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖帅矗,靈堂內(nèi)的尸體忽然破棺而出偎肃,到底是詐尸還是另有隱情,我是刑警寧澤浑此,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布累颂,位于F島的核電站,受9級(jí)特大地震影響凛俱,放射性物質(zhì)發(fā)生泄漏紊馏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一蒲犬、第九天 我趴在偏房一處隱蔽的房頂上張望朱监。 院中可真熱鬧,春花似錦原叮、人聲如沸赫编。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)擂送。三九已至,卻和暖如春唯欣,著一層夾襖步出監(jiān)牢的瞬間嘹吨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工境氢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蟀拷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓产还,卻偏偏與公主長(zhǎng)得像匹厘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子脐区,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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

  • **2014真題Directions:Read the following text. Choose the be...
    又是夜半驚坐起閱讀 9,511評(píng)論 0 23
  • 自我總結(jié): 顏色沒(méi)上好愈诚。下半部分太冷了,都帶著綠色調(diào)牛隅,層次不夠豐富炕柔。應(yīng)該再摻雜一點(diǎn)暖色調(diào)比如橙色什么的,陰影用紫色...
    喵喵僧閱讀 569評(píng)論 5 11
  • 你一定要知道的套路 微信號(hào) how13355789267 歡迎加好友交流 暗號(hào):商學(xué)院 消費(fèi)心理學(xué) 大家好媒佣,又和...
    精英之門閱讀 364評(píng)論 1 2
  • 我聽(tīng)到了 森林的失眠 田野的睡夢(mèng) 風(fēng)的竊竊私語(yǔ) 無(wú)由的 想喝杯酒 因?yàn)榈貌坏降?都會(huì)在酒里映現(xiàn) 你 是最浪漫的槍眼...
    執(zhí)念馥郁閱讀 165評(píng)論 0 1
  • 我意識(shí)到當(dāng)我無(wú)法說(shuō)服別人的時(shí)候匕累,一個(gè)很重要的前提是我無(wú)法說(shuō)服我自己! 我想說(shuō)不要因?yàn)楹闷嫘尿?qū)使下去嘗試一些本不該擁...
    浪跡_絕塵閱讀 174評(píng)論 0 0