借助 android databinding 框架,逃离 adapter 和 viewholder 的噩梦

借助 android databinding 框架,实现一个极简的 recyclerview adapter,支持多类型。并且演示使用这个 adpater 实现 HeaderItem, 刷新,加载更多,重试的完整示例。 —— 由Baurine分享

前言

理解这篇文章最好有 databinding 的基础知识,如果之前没有了解过,推荐下面三篇文章:
官方文档
connorlin 的中文翻译
棉花糖给 Android 带来的 Data Bindings

不多说,直接看一下最后的效果,UI 就别吐槽了,毕竟对于工程师来说,独立开发一个应用,最大的问题就是选颜色了(其实是我啦),本来想用一个 GIF 来展示所有效果的,但一个 GIF 体积太大了,所以就拆成了三个:
演示 HeaderItem,EmptyItem,ErrorItem,刷新,加载更多,加载出错并重试:

演示 没有更多数据:

稍微润色了一下 UI,为数据 Item 增加各种点击事件:

这个完整的例子包括了以下功能,我想应该能满足 90% 的需求吧:
刷新
加载更多
支持 header item
支持 empty item,并允许再次刷新
支持 error item,并允许再次刷新
支持 footer item,包括 3 种状态:加载中,出错并允许重试,没有更多数据
支持多种数据类型的 item,我们在这个例子中只展示了 ImageItem 和 TextItem 两种类型

下载 apk | 项目地址
这个项目中有两个文件夹,MultiTyepAdapterSample 和 MultiTypeAdapterTutorial,两者代码是几乎相同的,后者是我为了写这篇文章重新创建的,每一个关键步骤我都打好了 tag,以方便读者进行对照。
PS: 下面的内容完全是基于 databinding 思想来展开讲的,所以如果你对它不感兴趣,那么下面的内容对你就没什么帮助。另外,文章比较长且啰索 (力争讲解到每一个细节),需要比较充裕的时间来阅读和练习。如果你对 databinding 感兴趣,欢迎提交 issue 探讨。

大纲

实现篇
一步一步实现极简的 adapter

使用篇
设置 RecyclerView 和 SwipeRefreshLayout
实现各种状态类 item
实现刷新
实现加载更多
为 item 增加事件处理
获取 item 的 position
item 与 model 的关系

优化篇
将 adapter 独立成库
使用 MVP 简化 Activity 的逻辑

总结篇

实现篇

一步一步实现极简的 adapter
首先创建一个新的 Android Studio 工程,在 app module 的 build.gradle 中加上 databinding 的支持,并导入 recyclerview support 库:

// build.gradle
android { 
//... dataBinding { enabled = true }
}
dependencies { 
//... compile 'com.android.support:recyclerview-v7:25.1.0'
}

接着,我们来开始来实现这个 adapter,取名为 MultiTypeAdapter,因为我们要支持多类型,那么 adapter 里的 item 必然是抽象的,我们定义为 IItem:

// MultiTypeAdapter.java
public interface IItem {
}
private List<IItem> items = new ArrayList<>();

我们先从简单的入手,先来看看 getItemCount(),我们用 showHeader和 showFooter两个变量来控制是否显示 header 或 footer,那么 getItemCount()的实现如下:

// MultiTypeAdapter.java
@Override
public int getItemCount() {
  int cnt = items.size();
  if (showHeader) {
    cnt++;
  }
  if (showFooter) {
    cnt++;
  }
  return cnt;
}

接着来实现 getItemViewType(int position),关于这个方法,一般的实现,我们要根据 position 和相应位置的 item 类型来返回不同的值,比如:

// MultiTypeAdapter.java
@Override
public int getItemViewType(int position) {
    if (position == 0 && showHeader) {
      return ITEM_TYPE_HEADER;
    } else if (position == getItemCount() -1 && showFooter) {
      return ITEM_TYPE_FOOTER;
    } else {
      if (items.get(position) instanceof ImageItem) {
        return ITEM_TYPE_IMAGE;
      } else {
        return ITEM_TYPE_TEXT;
      }
    }
}

这样的实现,很烦很丑是不是。关于这个方法的优化,我们很容易达成一种共识,首先,我们不再返回类似 ITEM_TYPE_IMAGE这种常量类型,而是直接返回它的 xml layout,其次,我们直接从 item 自身得到这个 layout。因此,我们IItem 增加一个 getType()
的接口方法。代码如下:

public interface IItem {
  // should directly return layout
  int getType();
}

@Override
public int getItemViewType(int position) {
    if (position == 0 && showHeader) {
      return R.layout.item_header;
    } else if (position == getItemCount() -1 && showFooter) {
      return R.layout.item_footer;
    } else {
      return items.get(position).getType();
    }
}

(2017/2/15 Update: 由于 getType()实际是应该返回一个 xml layout 的,为了让这个方法名意义更明确,从 1.0.7 开始,这个方法重命名为 getLayout(),但整个教程仍然保留为 getType())
因为 header 和 footer,尤其是 footer,只是单纯地用来显示 正在 loading 等一些状态,我们很容易把它跟常规的数据 item 区别对待,但是,实际上我们可以把它看成一个伪 item,没有数据,只有布局的 item。我们分别实现只有布局的 HeaerItem 和 FooterItem,并在合适的时机加到 items 里面或从 items 里移除,就可以控制 header 和 footer 的显示与隐藏了。

// HeaerItem.java
public class HeaderItem implements MulitTypeAdapter.IItem {
    @Override
    public int getType() {
        return R.layout.item_header;
    }
}

// FooterItem.java
public class FooterItem implements MulitTypeAdapter.IItem {
    @Override
    public int getType() {
        return R.layout.item_footer;
    }
}

这样,我们的 getItemViewType()终于可以简化成一行代码了,清爽!

@Override
public int getItemViewType(int position) {
  return items.get(position).getType();
}

这样,我们也不需要 showHeader 和 showFooter 这样的状态变量了,那么 getItemCount()
也可以简化成一行代码了。

public int getItemCount() { 
  return items.size();
}

刚才说到我们要在合适的时机把 HeaerItem 或 FooterItem 加到 items 或从 items 中移除,所以我们给 adapter 加上一些操作 items 的方法。如下所示:

// MultiTypeAdapter.java
public void setItem(IItem item) {
    clearItems();
    addItem(item);
}

public void setItems(List<IItem> items) {
    clearItems();
    addItems(items);
}

public void addItem(IItem item) {
    items.add(item);
}

public void addItem(IItem item, int index) {
    items.add(index, item);
}

public void addItems(List<IItem> items) {
    this.items.addAll(items);
}

public void removeItem(IItem item) {
    items.remove(item);
}

public void clearItems() {
    items.clear();
}

你可能会想,诶,在这些操作函数里最后再加上 notifyDatasetChanged() 是不是会更方便点,这样我在上层就不用再手动调用一下 adapter.notifyDatasetChanged(),实际当你自己写起来的时候,你就会发现这样并不灵活。因为,我可能并不想每一次 addItem都刷新一次 UI,我可能要多次 addItem后才刷新一次 UI,这样,在上层由调用者来决定何时刷新 UI 会更灵活,更何况,我可能并不想只调用 notifyDatasetChanged(),我有时想调用 notifyItemRemoved(),或是 notifyItemChaned()。
当然,你也可以给 adapter 加上一个 getItems()的方法,然后把这些对 items 的操作逻辑都移动上层去处理,但我自己还是倾向于在 adapter 内封装这些方法。

// MulitTypeAdapter.java
public List<IItem> getItems() { 
  return items;
}

OK,至此,我们仅仅实现了 adapter 的 getItemCount()和 getItemViewType()方法,但是别着急。
截止目前为止的代码:tutorial_step_1

接下来,就该处理难啃的的 onCreateViewHolder()和 onBindViewHolder()了,先来看 onCreateViewHolder()吧。
在 onCreateViewHolder()中,我们要根据 viewType 来生成不同的 ViewHolder,假设这里我们只显示 ImageViewHolder 和 TextViewHolder。要显示的 item 分别为 ImageItem 和 TextItem。我们先定义一个抽象基类 ItemViewHolder,代码如下:

// ItemViewHolder.java
public abstract class ItemViewHolder extends RecyclerView.ViewHolder {
    public ItemViewHolder(View itemView) {
        super(itemView);
    }

    public abstract void bindTo(MulitTypeAdapter.IItem item);
}

分别实现 ImageViewHolder 和 TextViewHolder:

// ImageViewHolder.java
public class ImageViewHolder extends ItemViewHolder {
    public ImageViewHolder(View itemView) {
        super(itemView);
    }

    public void bindTo(MulitTypeAdapter.IItem item) {
        ImageItem imageItem = (ImageItem) item;
        // then do something
    }
}

// TextViewHolder.java
public class TextViewHolder extends ItemViewHolder {
    public TextViewHolder(View itemView) {
        super(itemView);
    }

    public void bindTo(MulitTypeAdapter.IItem item) {
        TextItem textItem = (TextItem) item;
        // then do something
    }
}

然后实现 onCreateViewHolder():

// MulitTypeAdapter.java
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView = LayoutInflater.from(parent.getContext())
            .inflate(viewType, parent, false);
    if (viewType == R.layout.item_image) {
        return new ImageViewHolder(itemView);
    } else if (viewType == R.layout.item_text) {
        return new TextViewHolder(itemView);
    }
    return null;
}

实现 onBindViewHolder():

// MulitTypeAdapter.java
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
    holder.bindTo(items.get(position));
}

可以看到,onBindViewHolder()的实现也已经变得非常简洁。那么就剩下 onCreateViewHolder()了。一般来说,我们会把这一部分逻辑通过工厂方法来优化,代码如下所示:

// ViewHolderFactory.java
public class ViewHolderFactory {
    public static ItemViewHolder create(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(viewType, parent, false);
        switch (viewType) {
            case R.layout.item_image:
                return new ImageViewHolder(itemView);
            case R.layout.item_text:
                return new TextViewHolder(itemView);
            default:
                return null;
        }
    }
}

那么 onCreateViewHolder 就可以同样简化成一行代码,如下所示:

public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  return ViewHolderFactory.create(parent, viewType);
}

截止目前为止的代码:tutorial_step_2

到目前为止,我们所做的和其它开发者所做的优化并没有什么不同,但是别着急,因为我们都还没有用上 databinding。接下来我们看看 databinding 的表现,看它是如何消除手动创建多个 ViewHolder 的。
我们要把 ImageItem 显示在 item_image.xml上,把 TextItem 显示在 item_text.xml上,我们分别用 databinding 的方式实现这两个 xml。在此之前,我们先来为 ImageItem 和 TextItem 填充一些数据。
借助 unsplash 提供的 url 让 ImageItem 产生随机图片 (别忘了在 AndroidManifest.xml 中加上网络访问权限),用当前日期时间作为 TextItem 的内容。

// ImageItem.java
public class ImageItem implements MulitTypeAdapter.IItem {
    @Override
    public int getType() {
        return R.layout.item_image;
    }

    ////////////////////////////////////////////////
    public final String url;

    public ImageItem() {
        url = "https://unsplash.it/200/200?random&" + new Random().nextInt(40);
    }
}

// TextItem.java
public class TextItem implements MulitTypeAdapter.IItem {
    @Override
    public int getType() {
        return R.layout.item_text;
    }

    ///////////////////////////////////////////
    public final String content;

    public TextItem() {
        content = new Date().toString();
    }
}

item_image.xml:

<layout>
    <data>
        <variable
            name="item"
            type="com.baurine.multitypeadaptertutorial.item.ImageItem"/>
    </data>

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:error="@{@drawable/ic_launcher}"
            app:imageUrl="@{item.url}"
            app:placeholder="@{@drawable/ic_launcher}"/>
    </LinearLayout>
</layout>

其中 ImageView 的 imageUrl/error/placeholder 属性是使用 强大的BindingAdapter 实现的,代码如下:

// BindingUtil.java
public class BindingUtil {
    @BindingAdapter({"imageUrl", "error", "placeholder"})
    public static void loadImage(ImageView imgView,
                                String url,
                                Drawable error,
                                Drawable placeholder) {
        Glide.with(imgView.getContext())
                .load(url)
                .error(error)
                .placeholder(placeholder)
                .into(imgView);
    }
}

item_text.xml:

<layout>
    <data>
        <variable
            name="item"
            type="com.baurine.multitypeadaptertutorial.item.TextItem"/>
    </data>

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{item.content}"/>
    </LinearLayout>
</layout>

在使用了 databinding 后,在创建 ViewHolder 时,ViewHolder 里需要保存就是不再是 itemView,而是 ViewDataBinding,每一个使用 <layout></layout>形式的 xml 布局都会被 databinding 框架自动生成一个 ViewDataBinding 类的派生类,比如 item_image.xml会生成 ItemImageBinding,item_text.xml会生成 ItemTextBinding,而 ViewDataBinding 是它们的基类。因此我们改写 ItemViewHolder/ImageViewHolder/TextViewHolder。

public abstract class ItemViewHolder extends RecyclerView.ViewHolder {
    protected final ViewDataBinding binding;

    public ItemViewHolder(ViewDataBinding binding) {
        super(binding.getRoot());
        this.binding = binding;
    }

    public abstract void bindTo(MulitTypeAdapter.IItem item);
}

public class ImageViewHolder extends ItemViewHolder {
    public ImageViewHolder(ViewDataBinding binding) {
        super(binding);
    }

    public void bindTo(MulitTypeAdapter.IItem item) {
        ImageItem imageItem = (ImageItem) item;
        ((ItemImageBinding) binding).setItem(imageItem);
    }
}

public class TextViewHolder extends ItemViewHolder {
    public TextViewHolder(ViewDataBinding binding) {
        super(binding);
    }

    public void bindTo(MulitTypeAdapter.IItem item) {
        TextItem textItem = (TextItem) item;
        ((ItemTextBinding) binding).setItem(textItem);
    }
}

此时,ViewHolderFactory 中的代码是这样的,我们要 inflate 得到ViewDataBinding,如下所示:

public class ViewHolderFactory {
    public static ItemViewHolder create(ViewGroup parent, int viewType) {
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
                viewType, parent, false);
        switch (viewType) {
            case R.layout.item_image:
                return new ImageViewHolder(binding);
            case R.layout.item_text:
                return new TextViewHolder(binding);
            default:
                return null;
        }
    }
}

截止目前为止的代码:tutorial_step_3

接下来,终于到了最关键最核心的一步,下面注意啦,我要开始变形啦。
在 ImageViewHolder 和 TextViewHolder 的 bindTo()方法中,我们分别进行了两次类型转化,但是,实际上,ViewDataBinding 为我们提供了一个另外一个更通用的方法 setVariable(int variableId, Object obj)来对 xml 中的变量进行赋值,注意,它的第二个参数是一个 Object。
比如,在 ImageViewHolder 中,我们持有了 item_image.xml对应的 ItemImageBinding 实例对象,我们可以用自动生成的 setItem((ImageItem)item)
方法来进行赋值,也可以使用 setVariable(BR.item, item)来进行赋值,因为这个 ViewDataBinding 实例知道,这个 xml 中 BR.item对应的类型是 ImageItem,所以它会自动把 item 转化成 ImageItem 类型。我们直接来看一下 ItemImageBinding 内部是怎么来实现 setVariable():

public boolean setVariable(int variableId, Object variable) {
    switch(variableId) {
        case BR.item :
            setItem((com.baurine.multitypeadaptertutorial.item.ImageItem) variable);
            return true;
    }
    return false;
}

可见,这个方法就是对各种 setXyz方法的一层封装。而因为这个方法是由基类 ViewDataBinding 定义的,根据 OOP 的多态特性, 我们直接调用基类的 setVariable()方法即可,因此,ImageViewHolder 中的 bindTo()方法就可以简化成一行代码:

public void bindTo(MulitTypeAdapter.IItem item) { 
    binding.setVariable(BR.item, item);
}

而对于 TextViewHolder 来说,也是一样的。如此一来,如果我们在不同的 item xml 中使用相同的 variable name,如上例中都使用了 name="item",那么 bindTo()方法就可以统一成一种写法了,如上面所示。
ImageViewHolder 和 TextViewHolder 从形式上已经是一样的了,那我们就没有必要实现多个 ViewHolder 了,统一用一个 ItemViewHolder 来实现,在 setVariable()后执行 binding.executePendingBindings()
来让 UI 马上变化:

public class ItemViewHolder extends RecyclerView.ViewHolder {
    private final ViewDataBinding binding;

    public ItemViewHolder(ViewDataBinding binding) {
        super(binding.getRoot());
        this.binding = binding;
    }

    public void bindTo(MulitTypeAdapter.IItem item) {
        binding.setVariable(BR.item, item);
        binding.executePendingBindings();
    }
}

但是我们一定要理解的是,单一 ViewHolder 的背后,是由 databinding 框架生成的多个 ViewDataBinding。总体上来说,代码量并没有减少,但对于我们开发者来说,要写的代码和逻辑确是大大减少了。
此时,ViewHolderFactory 可以简化成如下所示:

public class ViewHolderFactory {
    public static ItemViewHolder create(ViewGroup parent, int viewType) {
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
                viewType, parent, false);
        return new ItemViewHolder(binding);
    }
}

但是实际上,由于我们并不需要多个 ViewHolder 了,这个工厂类也就失去意义了,我们把 create()这个方法移到 ItemViewHolder 中,删除 ViewHolderFactory 类,并修改 adapter 的 onCreateViewHolder()方法,如下所示:

// ItemViewHolder.java
public static ItemViewHolder create(ViewGroup parent, int viewType) {
    ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
            viewType, parent, false);
    return new ItemViewHolder(binding);
}

// MulitTypeAdapter.java
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    return ItemViewHolder.create(parent, viewType);
}

更进一步,由于我们只有一个 ItemViewHolder,而且不需要对外公开,因此我们把它整体移入到 MulitTypeAdapter 类中,作为内部静态类。至此,整个 adapter 全部完成,全部代码如下所示 (tutorial_step_4
):

public class MultiTypeAdapter extends RecyclerView.Adapter<MultiTypeAdapter.ItemViewHolder> {

    public interface IItem {
        // should directly return layout
        int getType();
    }

    private List<IItem> items = new ArrayList<>();

    ///////////////////////////////////////////////////////

    @Override
    public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return ItemViewHolder.create(parent, viewType);
    }

    @Override
    public void onBindViewHolder(ItemViewHolder holder, int position) {
        holder.bindTo(items.get(position));
    }

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

    @Override
    public int getItemViewType(int position) {
        return items.get(position).getType();
    }

    ///////////////////////////////////////////////////////
    // operate items

    public List<IItem> getItems() {
        return items;
    }

    public void setItem(IItem item) {
        clearItems();
        addItem(item);
    }

    public void setItems(List<IItem> items) {
        clearItems();
        addItems(items);
    }

    public void addItem(IItem item) {
        items.add(item);
    }

    public void addItem(IItem item, int index) {
        items.add(index, item);
    }

    public void addItems(List<IItem> items) {
        this.items.addAll(items);
    }

    public void removeItem(IItem item) {
        items.remove(item);
    }

    public void clearItems() {
        items.clear();
    }

    ///////////////////////////////////////////////////
    static class ItemViewHolder extends RecyclerView.ViewHolder {
        private final ViewDataBinding binding;

        static ItemViewHolder create(ViewGroup parent, int viewType) {
            ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
                    viewType, parent, false);
            return new ItemViewHolder(binding);
        }

        ItemViewHolder(ViewDataBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        void bindTo(MultiTypeAdapter.IItem item) {
            binding.setVariable(BR.item, item);
            binding.executePendingBindings();
        }
    }
}

从此,我们就可以和 viewholder 说拜拜了,我们的重心转移到实现一个又一个的 Item 上,而 Item 是极为轻量的。
至此,我们一步一步地实现了这个目前还不到 100 行的极简 adapter,那如何使用它来,来轻松地实现 header, footer 呢,且听 下回 分解。
参考:优雅的实现多类型列表的Adapter
原文链接:https://github.com/baurine/multi-type-adapter/blob/master/note/multi-type-adapter-tutorial-1.md

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

推荐阅读更多精彩内容