Google官方Mvp架构详解(基于仿今日头条News项目)

先来一发Google官方Mvp架构地址:
https://github.com/googlesamples/android-architecture/tree/todo-mvp/

基类介绍

BaseFragment

这个类是Fragment的一级父类。主要完成以下功能:

  1. 在类的声明上添加了一个IBasePresenter的泛型,并且实现了IBaseView接口。

  2. 统一封装了EventBus的使用,在基类中注册和反注册EventBus。子类如果需要使用EventBus,则只需要添加相应的注解即可。(关于通过注解的方式在基类中封装EventBus,可以参考https://blog.csdn.net/xieluoxixi/article/details/78262765

  3. 在Fragment挂载在Activity的时候,保存了一个上下文Context对象。

  4. 在onCreate()方法中,调用了setPresenter()方法,来给Fragment设置它对应的Presenter,并且先于onCreateView()方法执行。(因为可能会在initData()、initView()、initEvent()方法中用到Presenter对象,避免空指针异常)

package com.example.think.base;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.think.utils.BindEventBus;

import org.greenrobot.eventbus.EventBus;

import butterknife.ButterKnife;

/**
 * Author: Funny
 * Description: This is 一级Fragment
 */
public abstract class BaseFragment<P extends IBasePresenter> extends Fragment implements IBaseView<P> {

    protected P mPresenter;

    protected Context mContext;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.mContext = context;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //设置Presenter对象
        setPresenter(mPresenter);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = LayoutInflater.from(getContext()).inflate(getLayoutId(), null);
        ButterKnife.bind(this, view);

        registEventBus();
        initData();
        initView(view);
        initEvent();
        return view;
    }

    private void registEventBus() {
        if (this.getClass().isAnnotationPresent(BindEventBus.class)) {
            EventBus.getDefault().register(this);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (this.getClass().isAnnotationPresent(BindEventBus.class)) {
            EventBus.getDefault().unregister(this);
        }
    }

    protected abstract int getLayoutId();

    protected void initData() {
    }

    protected void initView(View view) {
    }

    protected void initEvent() {
    }

}

IBasePresenter和IBaseView

接下来再来看看IBasePresenter和IBaseView里面的具体实现。这两个接口是MVP模式中Presenter和View的一级接口。其他的Presenter和View必须直接或者间接实现它。

这里有一个方法命名的小技巧,Presenter里面的方法全是do开头,而View里面的方法是on开头(想想也挺符合逻辑的,Presenter是干事的人,使劲的do,而View是被干的人,所以要on.......)。这样通过名字就可以知道是谁的方法,因为后面Activity或者Fragment里面的overrider方法太多,容易搞懵。

  • IBasePresenter

IBasePresenter里面有两个抽象方法,用于刷新数据和显示网络错误。这个功能方法和具体的业务逻辑有关,因为所有的Presenter都有这两个功能,其他更具体的功能,后面在它的子类中会说到。

package com.example.think.base;

/**
 * Author: Funny
 * Description: This is 一级Presenter接口
 */
public interface IBasePresenter {

    /**
     * 刷新数据
     */
    void doRefresh();

    /**
     * 显示网络错误
     */
    void doShowNetError();

}

  • IBaseView

IBaseView里面的方法和具体的业务逻辑有关,注释已经写得很清楚了

package com.example.think.base;

/**
 * Author: Funny
 * Description: This is 一级View接口
 */
public interface IBaseView<P> {
    /**
     * 显示加载动画
     */
    void onShowLoading();

    /**
     * 隐藏加载
     */
    void onHideLoading();

    /**
     * 显示网络错误
     */
    void onShowNetError();

    /**
     * 设置 presenter
     */
    void setPresenter(P presenter);

    /**
     * 绑定生命周期
     */
    //<X> AutoDisposeConverter<X> bindAutoDispose();
}

BaseListFragment

BaseListFragment是BaseFragment的子类。所有列表页Fragment都继承它。

BaseListFragment所继承的LazyLoadFragment是处理懒加载的,LazyLoadFragment同样继承自BaseFragment。只需要注意一个地方,在它的生命周期方法onActivityCreated()中,执行了一个抽象方法fetchData(),这个抽象方法就是界面加载数据的开始。

package com.example.think.base;

import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Toast;

import com.example.think.R;

import me.drakeet.multitype.Items;
import me.drakeet.multitype.MultiTypeAdapter;

/**
 * Author: Funny
 * Description: This is BaseListFragment
 */
public abstract class BaseListFragment<P extends IBasePresenter> extends LazyLoadFragment<P> implements IBaseListView<P>,SwipeRefreshLayout.OnRefreshListener{

    protected RecyclerView mRecyclerView;
    protected SwipeRefreshLayout mRefreshLayout;
    protected MultiTypeAdapter mAdapter;

    protected boolean canLoadMore = false;

    @Override
    protected int getLayoutId() {
        return R.layout.fragment_list;
    }

    @Override
    protected void initView(View view) {
        mRecyclerView = view.findViewById(R.id.recycler_view);
        mRefreshLayout = view.findViewById(R.id.refresh_layout);
        mRecyclerView.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(getContext());
        mRecyclerView.setLayoutManager(manager);
        //下拉刷新
        mRefreshLayout.setOnRefreshListener(this);
    }


    @Override
    public void onShowLoading() {
        /**
         * 列表Fragment,显示加载视图,设置mRefreshLayout的刷新状态为true
         */
        mRefreshLayout.post(() -> {
            mRefreshLayout.setRefreshing(true);
        });
    }

    @Override
    public void onHideLoading() {
        /**
         * 列表Fragment,隐藏加载视图,设置mRefreshLayout刷新状态为false
         */
        mRefreshLayout.post(() -> {
            mRefreshLayout.setRefreshing(false);
        });
    }

    @Override
    public void onShowNetError() {
        /**
         * 列表Fragment,加载时显示网络错误
         */
        Toast.makeText(getContext(), "网络不给力", Toast.LENGTH_SHORT).show();
        mAdapter.setItems(new Items());
        mAdapter.notifyDataSetChanged();
        canLoadMore = false;
    }

    @Override
    public void onShowNoMore() {
        /**
         * 列表Fragment,加载完毕,无更多数据
         */
        // TODO: 2018/9/6 无更多数据实现
        canLoadMore = false;
    }

    @Override
    public void onRefresh() {
        LinearLayoutManager manager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
        int firstVisibleItemPosition = manager.findFirstVisibleItemPosition();
        if (firstVisibleItemPosition == 0) {
            mPresenter.doRefresh();
            return;
        }
        mRecyclerView.scrollToPosition(5);
        mRecyclerView.smoothScrollToPosition(0);
    }
}

这里主要关注一下它所实现的IBaseListView接口,MVP模式有一个特点,一个View所需要完成的功能方法,往往都定义在它实现的View接口中。换句话说,就是IBaseListView接口规定了BaseListFragment需要实现哪些主要的功能(爸爸要你干啥,你就得乖乖干啥)。

注释已经写得很清楚了,前四个方法在BaseListFragment中处理,因为所有的加载动作都是一样的。后两个方法交给BaseListFragment的子类去实现,因为每个子类设置的presenter和adapter是不同的。

package com.example.think.base;

import java.util.List;

/**
 * Author: Funny
 * Time: 2018/8/27
 * Description: This is IBaseListView,列表Fragment中该有的行为
 */
public interface IBaseListView<T> extends IBaseView<T> {

    ///////////////////////////////////////////////////////////////////////////
    // 这四个方法在BaseListFragment中处理,因为所有的加载动作都是一样的
    ///////////////////////////////////////////////////////////////////////////
    /**
     * 显示加载动画
     */
    void onShowLoading();

    /**
     * 隐藏加载动画
     */
    void onHideLoading();

    /**
     * 显示网络错误
     */
    void onShowNetError();

    /**
     * 加载完毕
     */
    void onShowNoMore();


    ///////////////////////////////////////////////////////////////////////////
    // 这两个方法交给BaseListFragment的子类去实现,因为每个子类设置的presenter和adapter是不同的
    ///////////////////////////////////////////////////////////////////////////
    /**
     * 设置 presenter
     */
    void setPresenter(T presenter);

    /**
     * 设置适配器
     */
    void onSetAdapter(List<?> list);

}

MVP模式最主要的几个基类就是这些了。基类的好坏与否,直接决定项目结构的优劣。但是抽取基类往往是最难的部分,因为一开始你并不会对之后具体的业务逻辑那么了如指掌,所以你不能清楚的知道在基类的接口中写什么功能方法,哪些方法可以统一处理,哪些方法需要交给具体的子类单独处理......

我们能做的是,对照UI图,多看看接口文档,脑子里有一个成型的界面和界面的功能逻辑。尽量在写代码前,理清楚整个功能的思路,这非常重要。

或者有时候,当你写了一部分代码时,回过头来看,突然发现某一块代码可以抽取和优化,某一个功能可以抽取一个公共的接口方法放到基类里......

具体功能和界面

基类已经简单介绍过了,接着该实现具体的功能页面。

News

News

MD讲了这么多繁琐的基类,写了那么多代码,在手机上跑起来还是一片空白,真TM不爽。终于可以开始撸界面和功能了,不管那么多了,先撸一个RecyclerView列表把场面撑起来再说......

等等,在开始写功能代码前,我们还是需要再理一理这个界面和功能的具体实现思路,抽象成一个接口,把功能方法写在里面。先有优秀的爸爸,才能有更优秀的儿子。

IVideoContract契约类

这里使用的Google官方MVP的结构思想,先创建一个契约类IVideoContract,这个就是MVP的接口类,感觉就像是Model、View、Presenter三者签订某种契约,规定各自需要做的事情。里面写的都是抽象的功能方法,需要子类去具体实现。

契约类规定说:

Model长得太苦逼,性格又内向,网络请求和数据处理的重任就交给你了。

Presenter巧舌如簧,为人处世机灵圆滑,负责沟通和交互的工作。

View天生丽质,颜值高,你就是我们的形象代言人了。

package com.example.think.ui.video;

import com.example.think.base.IBaseListView;
import com.example.think.base.IBasePresenter;
import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.net.NetCallBack;

import java.util.List;

/**
 * Author: Funny
 * Description: This is MVP契约类
 */
public interface IVideoContract {

    interface View extends IBaseListView<Presenter> {

        /**
         * 请求数据
         */
        void onLoadData();

    }

    interface Presenter extends IBasePresenter {

        /**
         * 请求数据
         */
        void doLoadData(String categoryId);

        /**
         * 设置适配器
         */
        void doSetAdapter(List<MultiNewsArticleDataBean> datas);

        /**
         * 加载更多
         */
        void doLoadMoreData();

        /**
         * 加载完成
         */
        void doShowNoMore();

    }

    interface Model {

        /**
         * 网络请求
         */
        void loadNetData(String category, String time, NetCallBack<MultiNewsArticleDataBean> netCallBack);

    }

}

VideoArticleModel

Model的功能比较单一而繁重,负责做网络请求和数据处理。Model说谁叫自己长得丑,这种脏活累活体力活只能自己去做了。甚至于Model都要开启子线程来工作,不能挡了主线程的路(突然想到身为程序猿的自己不正是扮演着这么苦逼的角色么......)。

package com.example.think.ui.video;

import android.annotation.SuppressLint;

import com.example.think.bean.news.MultiNewsArticleBean;
import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.net.IMobileVideoApi;
import com.example.think.net.NetCallBack;
import com.example.think.net.RetrofitFactory;
import com.google.gson.Gson;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.android.ActivityEvent;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;

/**
 * Author: Funny
 * Description: This is VideoArticleModel网络请求
 */
public class VideoArticleModel implements IVideoContract.Model {

    @SuppressLint("CheckResult")
    @Override
    public void loadNetData(LifecycleProvider<ActivityEvent> provider, String category, String time, NetCallBack<MultiNewsArticleDataBean> netCallBack) {
        Observable<MultiNewsArticleBean> observable = RetrofitFactory.getInstance().create(IMobileVideoApi.class).getVideoArticle(category, time);
        observable.subscribeOn(Schedulers.io())
                .map(new Function<MultiNewsArticleBean, List<MultiNewsArticleDataBean>>() {
                    @Override
                    public List<MultiNewsArticleDataBean> apply(MultiNewsArticleBean multiNewsArticleBean) throws Exception {
                        List<MultiNewsArticleDataBean> dataList = new ArrayList<>();
                        List<MultiNewsArticleBean.DataBean> data = multiNewsArticleBean.getData();
                        for (MultiNewsArticleBean.DataBean datum : data) {
                            String content = datum.getContent();
                            dataList.add(new Gson().fromJson(content, MultiNewsArticleDataBean.class));
                        }
                        return dataList;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .compose(provider.bindToLifecycle())
                .subscribe(new Consumer<List<MultiNewsArticleDataBean>>() {
                    @Override
                    public void accept(List<MultiNewsArticleDataBean> multiNewsArticleDataBeans) throws Exception {
                        netCallBack.success(multiNewsArticleDataBeans);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        netCallBack.fail(throwable);
                    }
                });


    }
}

VideoArticleFragment

VideoArticleFragment对应MVP的View层,重点来关注一下重写的方法,来理一下它的执行顺序。顺序一定要对,不然可能会报空指针异常或者崩溃。

  1. onSetPresenter()方法是最先执行的,在BaseFragment中的onCreate()方法中执行。

  2. initData()、initView()、包括initEvent()方法是在onCreateView()方法中执行。

  3. fetchData()在onActivityCreated()方法中执行。

这几个方法是在生命周期方法里执行的,不需要我们手动调用。而在契约类IVideoContract中的onLoadData()方法,是需要手动调用,才会执行,所以需要搞清楚。(前面提到的,我们通过方法名前面一个on,就可以知道他是契约类IVideoContract中View层的方法。)

package com.example.think.ui.video;

import android.os.Bundle;
import android.view.View;

import com.example.think.base.BaseListFragment;
import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.viewHolder.news.NewsArticleVideoViewBinder;

import java.util.List;

import me.drakeet.multitype.Items;
import me.drakeet.multitype.MultiTypeAdapter;

/**
 * Author: Funny
 * Description: This is VideoArticleFragment
 */
public class VideoArticleFragment extends BaseListFragment<IVideoContract.Presenter> implements IVideoContract.View {

    private String mCategoryId;
    private Items mDatas = new Items();
    private MultiTypeAdapter mAdapter;


    public static VideoArticleFragment newInstance(String categoryId) {

        Bundle args = new Bundle();
        args.putString("categoryId", categoryId);
        VideoArticleFragment fragment = new VideoArticleFragment();
        fragment.setArguments(args);
        return fragment;
    }

    /**
     * 在BaseFragment中的onCreate方法中执行
     *
     * @param presenter
     */
    @Override
    public void onSetPresenter(IVideoContract.Presenter presenter) {
        if (mPresenter == null) {
            mPresenter = new VideoArticlePresenter(this);
        }
    }

    /**
     * 在onCreateView方法中执行
     */
    @Override
    protected void initData() {
        mCategoryId = getArguments().getString("categoryId");
    }

    @Override
    protected void initView(View view) {
        super.initView(view);
        mAdapter = new MultiTypeAdapter(mDatas);
        mAdapter.register(MultiNewsArticleDataBean.class, new NewsArticleVideoViewBinder());
        mRecyclerView.setAdapter(mAdapter);
    }

    /**
     * 在onActivityCreated方法中执行
     */
    @Override
    public void fetchData() {
        onLoadData();
    }


    @Override
    public void onLoadData() {
        onShowLoading();
        mPresenter.doLoadData(mCategoryId);
    }


    @Override
    public void onSetAdapter(List<?> list) {
        mDatas.clear();
        mDatas.addAll(list);
        mAdapter.notifyDataSetChanged();
        canLoadMore = true;
        mRecyclerView.stopScroll();
    }

}

VideoArticlePresenter

VideoArticlePresenter是负责Model和View的交互。在它的构造方法中,持有一个View的引用,同时还新创建了一个Model对象。有了这两个对象,让View(颜值担当)和Model(程序猿)发生一些关系,不是轻而易举的事情吗。

感觉这个交互很丝滑。

package com.example.think.ui.video;

import android.annotation.SuppressLint;
import android.text.TextUtils;

import com.example.think.bean.news.MultiNewsArticleDataBean;
import com.example.think.net.NetCallBack;
import com.example.think.utils.TimeUtil;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.android.ActivityEvent;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.Observable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Predicate;

/**
 * Author: Funny
 * Description: This is VideoArticlePresenter
 */
public class VideoArticlePresenter implements IVideoContract.Presenter {

    private VideoArticleModel mModel;
    private VideoArticleFragment mView;
    private String mTime;
    private List<MultiNewsArticleDataBean> mDatas;
    private String mCategory;
    private final LifecycleProvider<ActivityEvent> mProvider;

    public VideoArticlePresenter(VideoArticleFragment view) {
        mView = view;
        mModel = new VideoArticleModel();
        mTime = TimeUtil.getCurrentTimeStamp();
        mDatas = new ArrayList<>();
        mProvider = mView.autoRxLifeCycle();
    }

    @Override
    public void doLoadData(String categoryId) {
        mCategory = categoryId;
        mModel.loadNetData(mProvider,categoryId, mTime, new NetCallBack<MultiNewsArticleDataBean>() {
            @SuppressLint("CheckResult")
            @Override
            public void success(List<MultiNewsArticleDataBean> datas) {
                Observable.fromIterable(datas)
                        .filter(new Predicate<MultiNewsArticleDataBean>() {
                            @Override
                            public boolean test(MultiNewsArticleDataBean multiNewsArticleDataBean) throws Exception {
                                //这个时间参数用于加载更多
                                mTime = multiNewsArticleDataBean.getBehot_time();
                                String source = multiNewsArticleDataBean.getSource();
                                if (TextUtils.isEmpty(source)) {
                                    return false;
                                }

                                if (source.contains("头条") || source.contains("问答") || multiNewsArticleDataBean.getTag().contains("ad")) {
                                    return false;
                                }

                                //去除标题重复的新闻
                                for (MultiNewsArticleDataBean data : mDatas) {
                                    if (data.getTitle().equals(multiNewsArticleDataBean.getTitle())) {
                                        return false;
                                    }
                                }

                                return true;
                            }
                        })
                        .toList()
                        .compose(mProvider.bindToLifecycle())
                        .subscribe(new Consumer<List<MultiNewsArticleDataBean>>() {
                            @Override
                            public void accept(List<MultiNewsArticleDataBean> dataBeans) throws Exception {
                                if (dataBeans != null && dataBeans.size() > 0) {
                                    doSetAdapter(dataBeans);
                                } else {
                                    doShowNoMore();
                                }
                            }
                        });
            }

            @Override
            public void fail(Throwable throwable) {
                doShowNetError();
            }
        });
    }

    @Override
    public void doSetAdapter(List<MultiNewsArticleDataBean> datas) {
        mDatas.addAll(datas);
        mView.onHideLoading();
        mView.onSetAdapter(mDatas);
    }

    @Override
    public void doLoadMoreData() {
        //加载更多数据和category无关,只和当前时间有关,加载更多时,时间参数要用数据里的behot_time
        doLoadData(mCategory);
    }

    @Override
    public void doShowNoMore() {
        mView.onHideLoading();
        mView.onShowNoMore();
    }

    @Override
    public void doRefresh() {
        mDatas.clear();
        mView.onShowLoading();
        mTime = TimeUtil.getCurrentTimeStamp();
        doLoadData(mCategory);
    }

    @Override
    public void doShowNetError() {
        mView.onHideLoading();
        mView.onShowNetError();
    }
}

最后附上github地址,上面的代码都在里面。

https://github.com/FunnyLee/News

这个项目采用谷歌官方MVP架构实现,使用Android主流技术(Retrofit、RxJava、Material Design等等),并且我会持续更新......

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容