【Android】DataBinding(MVVM设计模式)

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

什么是MVVM

说到DataBinding,就有必要先提起MVVM设计模式。
Model–View–ViewModel(MVVM) 是一个软件架构设计模式,相比MVVM,大家对MVC或MVP可能会更加熟悉。

  • MVC:(VIew-Model-Controller)
    早期将VIew、Model、Controller代码块进行划分,使得程序大部分分离,降低耦合。
  • MVP:(VIew-Model-Presenter)由于MVC中View和Model之间的依赖太强,导致Activity中的代码过于臃肿。为了他们可以绝对独立的存在,慢慢演化出了MVP。在MVP中View并不直接使用Model,它们之间的通信是通过 Presenter (MVC中的Controller)来进行的。
  • MVVM:(Model–View–ViewModel)
    MVVM可以算是MVP的升级版,将 Presenter 改名为 ViewModel。关键在于View和Model的双向绑定,当View有用户输入后,ViewModel通知Model更新数据,同理Model数据更新后,ViewModel通知View更新。

Data Binding

在Google I/O 2015上,伴随着Android M预览版发布的Data Binding兼容函数库。
不知道要扯什么了,还是直接上代码,来看看Data Binding的魅力吧。

  • 环境要求

Data Binding对使用的环境还是有一定要求的(这货有点挑)
Android Studio版本在1.3以上
gradle的版本要在1.5.0-alpha1以上
需要在Android SDK manager中下载Android Support repository
然后在对应的Module的build.gradle中添加

android {
    ....
    dataBinding {
        enabled =true
    }
}

Gradle需要升级版本的可以参考升级Gradle版本

  • 创建对象

创建一个User类

public class User {
    private String firstName;
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return this.firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
}
  • 布局

在activity_main.xml中布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="com.example.gavin.databindingtest.User"/>
        <variable
            name="user"
            type="User" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"
            android:textSize="20sp" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.lastName}"
            android:textSize="25sp" />
    </LinearLayout>
</layout>

这里跟平时的布局有点不同,最外层是layout,里面分别是是data以及我们的布局。
data:声明了需要用到的user对象,type用于是定路径。
可以在TextView中的看到android:text="@{user.firstName}", 这是什么鬼,没见过这么写的!!!
(不急,继续往下看)

  • 绑定数据

看看下面的MainActivity

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        User user = new User("Micheal", "Jack");
        binding.setUser(user);
    }
}

问我ActivityMainBinding哪来的?我怎么知道...
ActivityMainBinding是根据布局文件的名字生成的,在后面加了Binding。
运行下看看效果吧

效果

有点懵逼了,就绑定了下而已,这些数据是怎么显示到界面上的。


懵逼

他是怎么工作的?
原来Data Binding 在程序代码正在编译的时候,找到所有它需要的信息。然后通过语法来解析这些表达式,最后生成一个类。
通过反编译我们可以看到,Data Binding为我们生成了databinding包,以及ActivityMainBinding类(反编译可以参考这里


看看我们在onCreate中最后调用的binding.setUser(user),在ActivityMainBinding中可以看到这个方法。
setUser方法

我想就是这个 super.requestRebind()对数据进行了绑定,至于里面怎么实现的,有待进一步研究。

更多用法

上面只是用一个简单的例子,展示了Data Binding的用法,如果想在实际项目中使用,可不是上面这例子可以搞定的。下面就来说说Data Bindig的更多用法。

  • 首先消除下大家对空指针的顾虑

自动生成的 DataBinding 代码会检查null,避免出现NullPointerException。
例如在表达式中@{user.phone}如果user == null 那么会为user.phone设置默认值null而不会导致程序崩溃(基本类型将赋予默认值如int为0,引用类型都会赋值null)

  • 自定义DataBinding名

如果不喜欢自动生成的Data Binding名,我们可以自己来定义

<data class="MainBinding">
    ....
</data>

class对应的就是生成的Data Binding名

  • 导包

跟Java中的用法相似,布局文件中支持import的使用,原来的代码是这样

<data>
     <variable name="user" type="com.example.gavin.databindingtest.User" />
 </data>

使用import后可以写成这样:

    <data>
        <import type="com.example.gavin.databindingtest.User"/>
        <variable
            name="user"
            type="User" />
    </data>

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
遇到相同的类名的时候:

<data>
    <import type="com.example.gavin.databindingtest.User" alias="User"/>
    <import type="com.example.gavin.mc.User" alias="mcUser"/>
    <variable name="user" type="User"/>
    <variable name="mcUser" type="mcUser"/>
</data>

使用alias设置别名,这样user对应的就是com.example.gavin.databindingtest.User,mcUser就对应com.example.gavin.mc.User,然后

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@{user.firstName}"/>

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
当需要用到一些包时,在Java中可以自动导包,不过在布局文件中就没有这么方便了。需要使用import导入这些包,才能使用。如,需要用到View的时候

<data>
    <import type="android.view.View"/>
</data>
...
<TextView
...
android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
/>

注意只要是在Java中需要导入包的类,这边都需要导入,如:Map、ArrayList等,不过java.lang包里的类是可以不用导包的

  • 表达式

在布局中,不仅可以使用

android:text="@{user.lastName}"

还可以使用表达式如:

三元运算

在User中添加boolean类型的isStudent属性,用来判断是否为学生。

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.isStudent? "Student": "Other"}'
android:textSize="30sp"/>

注意需要用到双引号的时候,外层的双引号改成单引号。
还可以这样用

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="学生"
android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
android:textSize="30sp"/>

这里用到的View需要在data中声明

<data>
     <import type="android.view.View"/>
</data>

注意:android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}",可能会被标记成红色,不用管它编译会通过的

??

除了常用的操作法,另外还提供了一个 null 的合并运算符号 ??,这是一个三目运算符的简便写法。

contact.lastName ?? contact.name

相当于

contact.lastName != null ? contact.lastName : contact.name

所支持的操作符如下:
数学运算符 + - / * %
字符串拼接 +
逻辑运算 && ||
二进制运算 & | ^
一元运算符 + - ! ~
位运算符 >> >>> <<
比较运算符 == > < >= <=
instanceof
Grouping ()
文字 - character, String, numeric, null
类型转换 cast
方法调用 methods call
字段使用 field access
数组使用 [] Arrary access
三元运算符 ? :

  • 显示图片

除了文字的设置,网络图片的显示也是我们常用的。来看看Data Binding是怎么实现图片的加载的。
首先要提到BindingAdapter注解,这里创建了一个类,里面有显示图片的方法。

public class ImageUtil {
    /**
     * 使用ImageLoader显示图片
     * @param imageView
     * @param url
     */
    @BindingAdapter({"bind:image"})
    public static void imageLoader(ImageView imageView, String url) {
        ImageLoader.getInstance().displayImage(url, imageView);
    }
}

(这方法必须是public static的,否则会报错)
这里只用了bind声明了一个image自定义属性,等下在布局中会用到。
这个类中只有一个静态方法imageLoader,里面有两参数,一个是需要设置图片的view,另一个是对应的Url,这里使用了ImageLoader库加载图片。
看看吧它的布局是什么样的吧

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

    <data >
        <variable
            name="imageUrl"
            type="String"/>
    </data>

    <LinearLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        >
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:image = "@{imageUrl}"/>
    </LinearLayout>
</layout>

最后在MainActivity中绑定下数据就可以了

binding.setImageUrl(
    "http://115.159.198.162:3000/posts/57355a92d9ca741017a28375/1467250338739.jpg");

哇靠!!!就这样?我都没看出来它是怎么设置这些图片的。
不管了,先看看效果。(其中的原理以后慢慢唠,这里就负责说明怎么使用,这篇已经够长了,不想再写了)

看个美女压压惊

使用BindingAdapter的时候,我这还出现了这样的提示,不过不影响运行。不知道你们会不会...


【已解决】
感谢颜路同学指出@BindingAdapter({"bind:image"}) 改成@BindingAdapter({"image"}) 就不会有警告了

  • 点击事件

在MainActivity中声明方法:

//参数View必须有,必须是public,参数View不能改成对应的控件,只能是View,否则编译不通过
public void onClick(View view) {
    Toast.makeText(this,"点击事件", Toast.LENGTH_LONG).show();
}

布局中:

    <data>
      ...
        <variable
        name="mainActivity"
        type="com.example.gavin.databindingtest.MainActivity"/>
    </data>
    ....
        <Button
            ...
            android:onClick="@{mainActivity.onClick}"
            />

最后记得在MainActivity中调用

binding.setMainActivity(this);

(发现:布局文件中,variable中的name,在binding中都会生成一个对应的set方法,如:setMainActivity。有set方法,那就应该有get方法,试试getMainActivity,还真有)
运行下看看效果

点击事件

当然如果你不想吧点击事件写在MainActivity中,你把它单独写在一个类里面:

public class MyHandler {
    public void onClick(View view) {
        Toast.makeText(view.getContext(), "点击事件", Toast.LENGTH_LONG).show();
    }
}
    <data>
      ...
        <variable
        name="handle"
        type="com.example.gavin.databindingtest.MyHandler"/>
    </data>
    ....
        <Button
            ...
            android:onClick="@{handle.onClick}"
            />
    </data>

在MainActivity调用

binding.setHandle(new MyHandler());
  • 调用Activity中的变量

上面看到它调用MainActivity中的onClick方法,那么可以调用MainActivity中的属性吗?
在MainActivity中定义mName,

public static String mName = "MM";

布局中

    <data>
        ...
        <variable
            name="mainActivity"
            type="com.example.gavin.databindingtest.MainActivity"/>
    </data>
        <Button
            ...
            android:text="@{mainActivity.mName}"
            />

注意这个变量必须是public static

  • 数据改变时更新UI

当数据发生变化时,我们可以这样更新UI

    private ActivityMainBinding binding;
    private User user;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        user = new User("Micheal", "Jack");
        binding.setUser(user);
        binding.setHandle(new MyHandler());
        delay();
    }
    /**
     * 两秒后改变firstName
     */
    private void delay() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                user.setFirstName("Com");
                binding.setUser(user);
            }
        }, 2000);
    }

看看调用的这个setUser是什么:


setUser

从反编译的代码中可以看出,setUser方法中重新绑定了数据。
看下效果

效果
  • BaseObservable

使用上面的代码实现了UI的更新你就满足了?其实官方为我们提供了更加简便的方式,使User继承BaseObservable,代码如下

public class User extends BaseObservable {
    private String firstName;
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Bindable
    public String getFirstName() {
        return this.firstName;
    }
    @Bindable
    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }
}

只要user发生变化,就能达到改变UI的效果。在MainActivity中只要调用以下代码

user.setFirstName("Com");

有了BaseObservable就够了?不不不,我比较懒,不想写那么多@Bindable和notifyPropertyChanged。万一里面有几十个属性,那不写哭起来?而且还有可能写丢了。
Data Binding的开发者贴心得为我们准备了一系列的ObservableField,包括: ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat,ObservableDouble, 以及 ObservableParcelable看看它们的用法
ObservableField的使用
1、创建User2

public class User2 {
    public final ObservableField<String> firstName = new ObservableField<>();
    public final ObservableField<String> lastName = new ObservableField<>();
    public final ObservableInt age = new ObservableInt();
    public final ObservableBoolean isStudent = new ObservableBoolean();
}

这类里面没有Get/Set。
2、布局文件

        <TextView
            ...
            android:text="@{user2.firstName}" />
        <TextView
            ...
            android:text="@{user2.lastName}" />
        <TextView
            ...
            android:text="@{String.valueOf(user2.age)}"
             />

3、MainActivity中

        mUser2 = new User2();
        binding.setUser2(mUser2);
        mUser2.firstName.set("Mr");
        mUser2.lastName.set("Bean");
        mUser2.age.set(20);
        mUser2.isStudent.set(false);

这里new了一个User2对象后,直接就绑定了。之后只要mUser2中的数据发生变化,UI也会随之更新。
除了这几个Map跟List也是必不可少的,Data Binding为我们提供了 ObservableArrayMapObservableArrayList
ObservableArrayMap的使用

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

ObservableArrayList的使用

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

在布局中使用中文时,编译无法通过。

android:text='@{user2.isStudent?"学生":"非学生"}'

感谢吕檀溪同学的解决方案:
这是java环境的问题,在系统环境变量中增加一个变量,变量名为: JAVA_TOOL_OPTIONS, 变量值为:-Dfile.encoding=UTF-8,保存。要重启一次电脑,中文就解决了,但是在某些地方,编译的时候控制台会出现部分乱

  • 在RecyclerView或ListView中使用

前面说了那么多基础的用法,可还是不能达到我们的需求。几乎在每个app中都有列表的存在,RecyclerView或ListView,从上面所说的似乎还看不出Data Binding在RecyclerView或ListView中是否也能起作用。(用屁股想也知道,Google的开发团对怎么可能会犯这么低级的错误)。下面以RecyclerView为例子:
1、直接看Item的布局(user_item.xml):

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="user2"
            type="com.example.gavin.databindingtest.User2" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user2.firstName}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="·"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user2.lastName}"/>
        <View
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="1"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{user2.age+""}'/>
    </LinearLayout>
</layout>

2、RecyclerView的数据绑定是在Adapter中完成的,下面看看Adapter(这里使用了一个Adapter,如果你在使用的时候发现RecyclerView的动画没了,去这里寻找答案)

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyHolder> {

    private List<User2> mData = new ArrayList<>();

    public MyAdapter(List<User2> data) {
        this.mData = data;
    }

    @Override
    public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return MyHolder.create(LayoutInflater.from(parent.getContext()), parent);
    }

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

    @Override
    public int getItemCount() {
        if (mData == null)
            return 0;
        return mData.size();
    }

    static class MyHolder extends RecyclerView.ViewHolder {
        private UserItemBinding mBinding;

        static MyHolder create(LayoutInflater inflater, ViewGroup parent) {
            UserItemBinding binding = UserItemBinding.inflate(inflater, parent, false);
            return new MyHolder(binding);
        }

        private MyHolder(UserItemBinding binding) {
            super(binding.getRoot());
            this.mBinding = binding;
        }

        public void bindTo(User2 user) {
            mBinding.setUser2(user);
            mBinding.executePendingBindings();
        }

    }
}

3、最后在布局和MainActivity中的使用跟平时的用法一样
布局中加入RecyclerView:

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

MainActivity中:

        List<User2> data = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            User2 user2 = new User2();
            user2.age.set(30);
            user2.firstName.set("Micheal " + i);
            user2.lastName.set("Jack " + i);
            data.add(user2);
        }
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(
                this, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setAdapter(new MyAdapter(data));

这样就可以了。
不过,在自动生成的ActivityMainBinding中,我们可以看到根据RecyclerView的id,会自动生成一个recyclerView。



所以在MainActivity中,我们可以不用findViewById,直接使用binding.recyclerView。

        LinearLayoutManager layoutManager = new LinearLayoutManager(
                this, LinearLayoutManager.VERTICAL, false);
        binding.recyclerView.setLayoutManager(layoutManager);
        binding.recyclerView.setAdapter(new MyAdapter(data));

来看看效果吧:


RecyclerView

Tips:

  • 1:若需要显示int类型,需要加上"":如

user.age为int类型,需要这样用

<TextView   
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text='@{""+user.age}'/>

<TextView   
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@{String.valueOf(user.age)}"/>
  • 2:不建议新手使用,出现错误的时候根据提示,不容易找到出错位置。(是根本找不到...)

参考

Google官方(权威,不过全英文。点击事件写的好像不对,后来去其他地方查的):
Realm(十分全面):
CSDN-亓斌(有点像google文档的翻译版,整体结果相似):
阳春面的博客(好奇怪的名字)

源码地址https://github.com/Gavin-ZYX/DataBindingTest

以上有错误之处感谢指出

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

推荐阅读更多精彩内容