android一种可配置的公共列表页封装方式

app项目中列表页面的封装实现

客户端项目常常遇到很多列表页面,而这些列表页面都很相似,无论逻辑还是ui,所以封装一个公用的列表页面可以极大地减轻开发负担,提高程序的可维护性。

列表页面的需求整理

  • 列表展示(必须支持)
  • 空页面展示(非必须)
  • 下拉刷新(非必须)
  • 上拉加载更多(非必须)
  • header展示(非必须)
  • 分割线(非必须)

上面就是一般情况下列表页面的核心需求,根据需求整理其公共逻辑和差异逻辑。

需求 公共逻辑 差异逻辑
列表展示 把列表每一个条目抽象成ItemView接口,把要展示的数据抽象成ViewBean接口 1.ui不同。2.点击事件处理不同
空白页面展示 空页面是一个view 1.就wdg来说,空页面有俩个元素—icon,和提示文字。其中只有提示文字有变化。
下拉刷新 下拉刷新页面(重新请求网络接口)
上拉加载更多 上拉加载更多 (请求分页数据)
header展示 header是一个view 1.ui不同。 2.点击事件不同。3.其它复杂交互
decoration 分割线展示 1.左右间距。2.颜色。3.粗细。4.挨着header的分割线和底部分割线的绘制控制

实现方式

  • 所有特性可配置

上面分析了列表页需要支持的特性,但是实际需求可能是某几个特性的组合,或者全部支持。这样把所有特性都做成可配置的,那么就比较灵活。

所以定义了一个ListFragmentConfig类,其成员变量为支持的特性和其它变量(列表元素的布局,header的布局等等)

  • 上下拉的支持

下拉刷新和上拉支持更多用开源库RefreshLayout支持。在view层上下拉的操作是一样的,变化的是数据接口以及数据不同,这里委托给Presenter层来处理。

  • 空页面和header的支持

header的支持—— 使用WrapperedAdapter实现,本质上还是RecyclerView.Adapter,每一个header都是一种类型的ItemView。header除了展示ui,还需要对数据进行一些操作,这里需要对Presenter进行操作。

  • 列表展示

列表展示最主要的变化是数据不同每一项展示的ui不同,另外点击事件的处理不同,或许还有更复杂的逻辑处理。对于这些变化我们不在公共列表页处理,委托给其它view层元素(headerview,parentFragment,Activity等等)

根据上述分析,抽象出几个接口

  1. ItemView—— 表示列表中的一个ui元素
  2. ViewBean—— 表示列表元素要渲染到界面的数据
  3. UsePresenter—— 表示要使用Presenter
  4. 定义一个公共适配器把ItemViewViewBean联系起来
public class CommonRecyclerViewAdapter<VB extends ViewBean> extends RecyclerView.Adapter<CommonRecyclerViewAdapter.ViewHolder<? extends ItemView>>

几个接口的具体代码如下

public interface ItemView<T extends ViewBean> {
    void bindData(T bean);
}

public interface ViewBean extends Serializable{
}

public interface UsePresenter<T extends BasePresenter> {
    default void attachPresenter(T presenter) {}
}
  • MVP架构

定义公共列表的Presenter层BaseListPresenter,抽象出其公共逻辑—— 设置请求参数,刷新,加载更多 ,消息总线订阅和解订阅。

public interface BaseListPresenter extends BasePresenter {
    
    /**
     * 刷新数据
     */
    default void refreshRequest(){};
    
    /**
     * 加载更多数据
     */
    default void loadMore(){};

    /**
     * 设置额外的参数
     */
    default void setExtraParams(Map extraParams){}

    default void registBus(){
        if (!EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().register(this);
        }
    }

    default void unRegisterBus(){
        if (EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().unregister(this);
        }
    }
}

定义公共列表的View层BaseListView,抽象出其公共逻辑—— 展示数据列表到ui上,根据数据渲染header,展示空页面,清空列表。

public interface BaseListView extends BaseView {
    void renderListData(List<? extends ViewBean> viewBeans, boolean isRefresh, boolean hasMore);

    default void renderHeaderView(ViewBean viewBean){}

    default void showEmptyView(){};

    default void clearList(){};
}

上述就是实现层面的一些抽象设计。具体代码和其它细节处理可查看wdg项目相关源码。

使用例子

以wdg项目交易页下部分的几个tab为例

        //持仓
        ListFragmentConfig config = ListFragmentConfig.newBuilder()
            .orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_hold)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportRefresh(false)
            .isSupportLoadMore(false)
            .isAutoSubsribe(false)
            .isSupportEventBus(true)
            .listHeaderRes(R.layout.wdgapp_header_trade_hold)
            .isSupportEmpty(true)
            .aClass(WdgTradeHoldPresenter.class)
            .extraParams(extraParams)
            .emptyMsg(getString(R.string.wdgapp_no_holds))
            .build();
        WdgListFragment listFragment = WdgListFragment.newInstance(config);

        //挂单
        config = ListFragmentConfig.newBuilder()
            .orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_order)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportEventBus(true)
            .isAutoSubsribe(false)
            .isSupportLoadMore(false)
            .extraParams(extraParams)
            .aClass(WdgTradePendingOrderPresenter.class)
            .build();
        WdgListFragment pendingOrderFragment = WdgListFragment.newInstance(config);

        //委托
        config = ListFragmentConfig.newBuilder().orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_order)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportLoadMore(true)
            .extraParams(extraParams)
            .aClass(WdgTradeEntrustOrderPresenter.class)
            .build();
        WdgListFragment entrustFragment = WdgListFragment.newInstance(config);

        //成交
        config = ListFragmentConfig.newBuilder().orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_deal)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportLoadMore(false)
            .extraParams(extraParams)
            .aClass(WdgTradeDealPresenter.class)
            .build();
        WdgListFragment dealFragment = WdgListFragment.newInstance(config);

我们只需要根据需求配置即可,不关心列表页的具体实现。至于其它的变化我们可按如下一套规范来开发,以交易页持仓列表为例

  • 数据层的实现—— 根据接口新建ViewBean的实现类(列表元素要展示的数据bean)。

    public static class Hold implements ViewBean {
            @SerializedName("currency_id")
            public String currencyId;
    
            @SerializedName("currency_mark")
            public String currencyMark;
    
            public String num;
    
            public String forzennum;
    
            @SerializedName("currency_all_money")
            public String currencyAllMoney;
    
            public String price;
    }
    
  • 数据请求的实现—— 定义一个BaseListPresenter的实现类,实现其“刷新”/“加载更多”逻辑,根据需求实现其它逻辑。

    public class WdgTradeHoldPresenter implements BaseListPresenter {
    
        private BaseListView mView;
        private String mCurrencyId;
        private CompositeDisposable mComposite;
    
        public WdgTradeHoldPresenter(BaseListView mView) {
            this.mView = mView;
        }
    
        @Override
        public void setExtraParams(Map extraParams) {
            if (extraParams == null) {
                return;
            }
            mCurrencyId = (String) extraParams.get("currencyId");
        }
    
        @Override
        public void subscribe() {
            registBus();
            visible();
        }
    
        private void visible() {
            if (mComposite == null || mComposite.isDisposed()) {
                mComposite = new CompositeDisposable();
            }
            Disposable timerDispose = Flowable.interval(0, 3, TimeUnit.SECONDS)
                    .onBackpressureDrop()
                    .observeOn(SchedulerProvider.getInstance().ui())
                    .subscribe(aLong -> {
                        //请求持仓数据
                         Disposable dispose = NetworkManager.getInstance().getApiInterface()
                                .getTradeHold("")
                                .compose(SchedulerProvider.getInstance().applySchedulers())
                                .subscribe(response -> {
                                    mView.renderListData(response.getChichang(), true, false);
                                }, throwable -> {});
                        mComposite.add(dispose);
                    }, throwable -> {});
            mComposite.add(timerDispose);
        }
    
        @Override
        public void unsubscribe() {
            unRegisterBus();
            invisible();
        }
    
        private void invisible() {
            disPose(mComposite);
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventHoldResponse(WdgTradeHoldResponse response) {
            mView.renderListData(response.getChichang(), true, false);
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventSubscribe(WdgSubscribeEvent event) {
            if (event.isSubscribe()) {
                this.visible();
            } else {
                this.invisible();
            }
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventCancelOrder(WdgCancelTradeEvent event) {
            refreshRequest();
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventOrderSuccess(WdgOrderSuccessEvent event) {
            refreshRequest();
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventTabChange(WdgTradeTabChangeEvent event) {
            if (event.pageIndex == 0) {
                this.visible();
            }
        }
    }
    
  • 根据设计图实现xml

    <?xml version="1.0" encoding="utf-8"?>
    <com.wdg.tradecenter.pages.trade.widgets.WdgTradeHoldRvItemView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="55dp"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">
    
        <android.support.constraint.Guideline
            android:id="@+id/guideline"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.5466" />
    
        <TextView
            android:id="@+id/tv_ico_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="WDG"
            android:textColor="@color/wdgapp_color_333333"
            android:textSize="15sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginLeft="15dp"
            />
    
        <TextView
            android:id="@+id/tv_quantity"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="10000"
            android:textColor="@color/wdgapp_color_333333"
            android:textSize="14sp"
            app:layout_constraintRight_toLeftOf="@+id/guideline"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/tv_frozen"
            app:layout_constraintVertical_chainStyle="packed"
            />
    
        <TextView
            android:id="@+id/tv_frozen"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="10"
            android:textColor="@color/wdgapp_color_999999"
            android:textSize="10sp"
            app:layout_constraintRight_toLeftOf="@+id/guideline"
            app:layout_constraintTop_toBottomOf="@+id/tv_quantity"
            android:layout_marginTop="1dp"
            app:layout_constraintBottom_toBottomOf="parent"
            />
    
        <TextView
            android:id="@+id/tv_value"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="10000"
            android:textColor="@color/wdgapp_color_333333"
            android:textSize="14sp"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/tv_price"
            app:layout_constraintVertical_chainStyle="packed"
            android:layout_marginRight="15dp"
            />
    
        <TextView
            android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="10"
            android:textColor="@color/wdgapp_color_999999"
            android:textSize="10sp"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_value"
            android:layout_marginTop="1dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginRight="15dp"
            />
    
        <View
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:background="@color/wdgapp_color_eeeeee"
            android:layout_marginLeft="15dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            />
    
    </com.wdg.tradecenter.pages.trade.widgets.WdgTradeHoldRvItemView>
    
  • 根据xml新建一个ItemView的实现类,实现其"绑定数据"逻辑。

    public class WdgTradeHoldRvItemView extends ConstraintLayout implements ItemView<WdgTradeHoldResponse.Hold> {
        @BindView(R.id.tv_ico_name)
        TextView tvIcoName;
        @BindView(R.id.tv_quantity)
        TextView tvQuantity;
        @BindView(R.id.tv_frozen)
        TextView tvFrozen;
        @BindView(R.id.tv_value)
        TextView tvValue;
        @BindView(R.id.tv_price)
        TextView tvPrice;
    
        public WdgTradeHoldRvItemView(Context context) {
            super(context);
        }
    
        public WdgTradeHoldRvItemView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public WdgTradeHoldRvItemView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            ButterKnife.bind(this);
        }
    
        @Override
        public void bindData(WdgTradeHoldResponse.Hold bean) {
            if (bean == null) {
                return;
            }
            tvIcoName.setText(bean.getCurrencyMark());
            tvQuantity.setText(FormatUtil.formatWdgQuantity(bean.getNum()));
            tvFrozen.setText(FormatUtil.formatWdgQuantity(bean.getForzennum()));
            tvValue.setText(bean.getCurrencyAllMoney());
            tvPrice.setText(bean.getPrice());
        }
    }
    
  • 最后把这些变化和需求所要支持的特性配置,然后WdgListFragment根据这些配置构建列表页

    //持仓
            ListFragmentConfig config = ListFragmentConfig.newBuilder()
                .orientation(OrientationHelper.VERTICAL)
                .viewBeanRes(R.layout.wdg_itemview_trade_hold)
                .listEmptyRes(R.layout.wdg_trade_empty_no_record)
                .isSupportRefresh(false)
                .isSupportLoadMore(false)
                .isAutoSubsribe(false)
                .isSupportEventBus(true)
                .listHeaderRes(R.layout.wdgapp_header_trade_hold)
                .isSupportEmpty(true)
                .aClass(WdgTradeHoldPresenter.class)
                .extraParams(extraParams)
                .emptyMsg(getString(R.string.wdgapp_no_holds))
                .build();
            WdgListFragment listFragment = WdgListFragment.newInstance(config);
    

更多使用姿势

  • 点击事件的处理,实现CommonRecyclerViewAdapter.OnRecyclerViewItemClickListner<WdgMarketListResponse.Currency>接口

    @Override
        public void onItemClick(View view, RecyclerView rv, int position, WdgMarketListResponse.Currency data) {
            WdgCoinDetailActivity.start(getContext(), data.currency_id);
        }
    
  • ItemView中的多个控件的点击事件处理—— 重写点击事件,并把点击监听派发给需要处理的控件

    @Override
        public void setOnClickListener(@Nullable OnClickListener l) {
            tvCancel.setOnClickListener(l);
        }
    
  • header实现

    public class WdgIcoPropertyHeaderView extends ConstraintLayout implements ItemView<WdgPropertyResponse> {
    
        private TextView tvAll;
        private ImageView ivHide;
    
        private FrameLayout flDeposit;
        private FrameLayout flWithDraw;
    
        private WdgPropertyResponse mResponse;
    
        public WdgIcoPropertyHeaderView(Context context) {
            super(context);
        }
    
        public WdgIcoPropertyHeaderView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public WdgIcoPropertyHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            tvAll = findViewById(R.id.tv_property_content);
            ivHide = findViewById(R.id.iv_crypt_state);
            flDeposit = findViewById(R.id.fl_deposit);
            flWithDraw = findViewById(R.id.fl_withdraw);
    
            flDeposit.setOnClickListener(v -> {
                WdgChargeCoinActivity.start(getContext());
            });
    
            flWithDraw.setOnClickListener(v -> {
                Context context = getContext();
                context.startActivity(new Intent(context, WdgWithdrawCoinActivity.class));
            });
    
            ivHide.setOnClickListener(v -> {
                boolean showMoney = SPUtils.getInstance().getBoolean(WdgCons.SP_KEY_SHOW_MONEY, true);
                showMoney = !showMoney;
                SPUtils.getInstance().put(WdgCons.SP_KEY_SHOW_MONEY, showMoney);
                updateMoneyAboutView(showMoney);
                RecyclerView rv = getParentRecyclerView(this);
                if (rv != null && rv.getAdapter() != null) {
                    rv.getAdapter().notifyDataSetChanged();
                }
            });
        }
    
        @Override
        public void bindData(WdgPropertyResponse bean) {
            if (bean == null || !bean.isSucceed()) {
                return;
            }
            mResponse = bean;
            boolean showMoney = SPUtils.getInstance().getBoolean(WdgCons.SP_KEY_SHOW_MONEY, true);
            updateMoneyAboutView(showMoney);
        }
    
        private void updateMoneyAboutView(boolean showMoney) {
            if (mResponse == null) {
                return;
            }
            if (showMoney) {
                tvAll.setText(mResponse.getTotalMoneyRaw());
                ivHide.setImageResource(R.drawable.wdgapp_icon_property_state_show);
            } else {
                ivHide.setImageResource(R.drawable.wdgapp_icon_property_state_hide);
                tvAll.setText(WdgPropertyFragment.CRYPT_SYMBOLS);
            }
        }
    
        private static RecyclerView getParentRecyclerView(@Nullable View view) {
            if (view == null) {
                return null;
            }
            ViewParent parent = view.getParent();
            if (parent instanceof RecyclerView) {
                return (RecyclerView) parent;
            } else if (parent instanceof View) {
                return getParentRecyclerView((View) parent);
            } else {
                return null;
            }
        }
    }
    

总结

本文整理了公共列表页的需求,设计和实现过程,使用的例子,以及使用规范。当然在实现细节和需求满足上还有很多瑕疵,希望大家共同完善,在保留接口不变的情况下提出更好的代码实现。尽管这个小框架不太完善,但优点也是显而易见的:

  • 把类似需求的变化项分解成清晰的互不相干的粒度进行处理,把公共的逻辑统一处理,提供更清晰的代码实现思维,减少出错的机率
  • 更好的维护性—— 假如需要修改空页面的样式,我们可以统一处理,而不必修改多处代码。对于特殊的空页面,框架也支持自定义布局设置进去/或者自己控制header进行show和hide的操作。

后续支持的特性

  • 灵活的分割线支持
    列表的参数类是ListFragmentConfig,分割线的参数类是DecorationConfig.如下所示
 DecorationConfig implements Serializable {
     int dividerHeight;          //分割线的高度
    float leftOffset;              //分割线离左边框的距离
    float rightOffset;           //分割线离右边框的距离
    int dividerColorRes;    //分割线的颜色
    int style;                     //分割线的风格,比如头部不含分割线,从第二个view开始绘制分割线等
    ...
}

 // 分割线的使用
  DecorationConfig decorationConfig=new DecorationConfig.Builder()
                .dividerHeight(1)
                .leftOffset(0)
                .rightOffset(0)
                .dividerColorRes(R.color.wdgapp_color_eeeeee)
                .style(WdgRVItemSplitDecoration.STYLE_NO_FIRST_DIVIDER)
                .build();

        config = ListFragmentConfig.newBuilder().orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_order)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportLoadMore(true)
            .decorationConfig(decorationConfig)     //分割线设置
            .extraParams(extraParams)
            .aClass(WdgTradeEntrustOrderPresenter.class)
            .build();
        WdgListFragment entrustFragment = WdgListFragment.newInstance(config);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354