有关Databinding与MVVM的一些事

DataBinding

说到DataBinding,大家就会想到双向绑定。那究竟什么是双向绑定,其实对于刚接触的人来说是需要去理解一下的。

MVVM中,ViewModel是互相隔离的。假设有以下的EditText布局:

android:text="@{echo.text}"

那么显而易见,当echo.text发生变化时,我们希望EditText中的android:text属性可以自动变化。这是Model层->View层的绑定。

反之,当用户通过View层在EditText中进行输入时,我们希望echo.text字段也可以同步更新到最新的输入值。这是ViewModel层的绑定。这一点很多人都会忽略。

那么首先我们看是Model层->View层的绑定是怎么实现的,在DataBinding中大家都知道有以下两种方式:

  • Model继承BaseObservable,get带上@Bindable注解
  • 相应字段使用Observablexxx变量,如下text字段(其实Observablexxx就是继承BaseObservable)

可以简单看下BaseObservable的源码,后面会用到:

public class BaseObservable implements Observable {
   @Override
    public void addOnPropertyChangedCallback(@NonNullOnPropertyChangedCallback callback) {
        ...
        mCallbacks.add(callback);
    }
   
    public void notifyPropertyChanged(int fieldId) {
        ...
        mCallbacks.notifyCallbacks(this, fieldId, null);
    }

就是一个回调模式。

public class Echo {
  public ObservableField<String> text = new ObservableField<>();
}

当使用ObservableField后,就真正使用了观察者的模式。也就是说当调用setEcho方法后,一个监听器就被注册了,这个监听器会在每次text字段被更新后去更新视图。

先来一段小小的源码分析,每个layout生成的xxxBinding都是关键的类,里面有一个executeBinding方法。

我们来看,一个简单的

  • android:text="@{model.str}"

会生成什么模板代码?

   @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        java.lang.String modelStr = null;
        me.lizz0y.myapplication.VM model = mModel;

        if ((dirtyFlags & 0x3L) != 0) {
                if (model != null) {
                    // read model.str
                    modelStr = model.str;
                }
        }
        // batch finished
        if ((dirtyFlags & 0x3L) != 0) {
            // api target 1
            android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);
        }
    }

关键代码:

modelStr = model.str;
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);//这个`xxxAdapter`后面再说,这里就看出简单的赋值就好了

如果变成

  • public ObservableField<String> str = new ObservableField<String>("sss");

我们会发现,在executeBinding中多了一句:

updateRegistration(0, modelStr);//localFieldId

将这个变量的fieldIdmodel.str这个Observable绑定在一起,同时使用前面的addOnPropertyChangedCallbackViewBinding类作为回调传进去。

最终,当我们对mode.str进行set操作时,一系列回调最终走到ViewDataBinding

   @Override
    protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
        switch (localFieldId) {
            case 0 :
                return onChangeModelStr((android.databinding.ObservableField<java.lang.String>) object, fieldId);
        }
        return false;
    }

其实就是对str这个变量赋予脏位,让下次屏幕刷新时更新这个变量对应的View

介绍一些常用的运算符:

运算符

@BindingConversion

如果在xml里我这么写:android:background="@{@color/blue}"

会报错,因为background应该是drawable。所以要进行自动转化,所以需要进行如下定义:

//转化@color/blue为drawable
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

经过前面分析也很简单:

android.databinding.adapters.ViewBindingAdapter.setBackground(
this.mboundView0,me.lizz0y.myapplication.VM.convertColorToDrawable(
    mboundView0.getResources().getColor(R.color.blue)));
        

当然这里显然有人会问,假设我定义了多个怎么破,结论就是后面的覆盖前面的。。。

@BindAdapter

举个栗子就明白了:

@BindingAdapter({"imageUrl"})  
public static void loadImage(ImageView view, String u) {  
    RequestOptions options = new RequestOptions()  
            .centerCrop()  
            .placeholder(R.mipmap.ic_launcher_round)  
            .error(R.mipmap.ic_launcher)  
            .priority(Priority.HIGH)  
            .diskCacheStrategy(DiskCacheStrategy.NONE);  

    Glide.with(view.getContext()).applyDefaultRequestOptions(options).load(u).transition(new DrawableTransitionOptions().crossFade(1000)).into(view);  
}  

xml里这么写:

 <ImageView  
    android:layout_width="100dp"  
    android:layout_height="100dp"  
    app:imageUrl="@{user.url}" />  

这就每次更新user.url时就会自动重新设置图片。

DataBindingset属性attr时会先看View有没有setXXX方法。如果没有就去找有没有BindingAdapter注解设置对应的方法。

前面分析源码的时候提到过

if ((dirtyFlags & 0x3L) != 0) {
    // api target 1
    android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);
}

我们看这里的BindingAdapter

@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
    final CharSequence oldText = view.getText();
    if (text == oldText || (text == null && oldText.length() == 0)) {
        return;
    }
    if (text instanceof Spanned) {
        if (text.equals(oldText)) {
            return; // No change in the spans, so don't set anything.
        }
    } else if (!haveContentsChanged(text, oldText)) {
        return; // No content changes, so don't set anything.
    }
    view.setText(text);
}

可以看到会比较oldText&newText,以防无限循环。

Component

直接看这篇吧,写的很好

我们可以定义多个BindingAdapter,但究竟想要哪个发挥作用呢? 就可以使用这个Component

BindingMethod

该注解可以帮助我们重新命名view属性对应的setter方法名称。

@BindingMethods({@BindingMethod(type = NestedScrollView.class, attribute = "custom", method = "setMyCustomAttr")})

注解在类上。

个人觉得他与BindingAdapter的区别在于:

  • 参数,BindingAdapter可以拿到view引用
  • component

监听属性变更

databinding让我们省去了各种监听函数,但有的时候我们需要在属性变化时做一些额外的事情:

mModel.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
    @Override
    public void onPropertyChanged(Observable observable, int i) {
        if (i == BR.name) {
            Toast.makeText(TwoWayActivity.this, "name changed",
                    Toast.LENGTH_SHORT).show();
        } else if (i == BR.password) {
            Toast.makeText(TwoWayActivity.this, "password changed",
                    Toast.LENGTH_SHORT).show();
        }
    }
});

双重绑定

下面我们来说说如果从View->Model的绑定。我们看在前面已有的基础上,我们如何自己实现双重绑定

<EditText
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:hint="Text 1"
  android:text="@{echo.text}"
  android:addTextChangedListener="@{echo.watcher}"/>

假设有两个EditText都使用了android:text,可以看到一个改变并不能连带带动另一个EditText,因为EditText的输入并没有手动去调用setField方法。所以显而易见需要再使用DataBinding赋予textChangedListener

 public TextWatcher watcher = new TextWatcherAdapter() {
    @Override public void afterTextChanged(Editable s) {
      if (!Objects.equals(text.get(), s.toString())) { //防止无限循环
        text.set(s.toString());
      }
    }
  };
customBinding
public class BindableString extends BaseObservable {
  private String value;
  public String get() {
    return value != null ? value : “”;
  }
  public void set(String value) {
    if (!Objects.equals(this.value, value)) {
      this.value = value;
      notifyChange();
    }
  }
  public boolean isEmpty() {
    return value == null || value.isEmpty();
  }
}

@BindingConversion
public static String convertBindableToString(
    BindableString bindableString) {
  return bindableString.get();
}

当主动调用BindableString.set时会通过notifyChange去触发UI更新,UI更新时调用convertBindableToString取出string绑定

或者BindAdapter:

@BindingAdapter({“app:binding”})
public static void bindEditText(EditText view,
    final BindableString bindableString) {
  Pair<BindableString, TextWatcherAdapter> pair = 
    (Pair) view.getTag(R.id.bound_observable);
  if (pair == null || pair.first != bindableString) {
    if (pair != null) {
     view.removeTextChangedListener(pair.second);
    }
    TextWatcherAdapter watcher = new TextWatcherAdapter() {
      public void onTextChanged(CharSequence s, 
          int start, int before, int count) {
        bindableString.set(s.toString());
      }
    };
    view.setTag(R.id.bound_observable, 
       new Pair<>(bindableString, watcher));
    view.addTextChangedListener(watcher);
  }  
  String newValue = bindableString.get();
  if (!view.getText().toString().equals(newValue)) {
    view.setText(newValue);
  }
}
<EditText
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:hint="Text 1"
  app:binding="@{echo.text}"/>

或者可以使用BindAdapter:

@BindingAdapter({“app:binding”})
public static void bindEditText(EditText view,
    final BindableString bindableString) {
  Pair<BindableString, TextWatcherAdapter> pair = 
    (Pair) view.getTag(R.id.bound_observable);
  if (pair == null || pair.first != bindableString) {
    if (pair != null) {
     view.removeTextChangedListener(pair.second);
    }
    TextWatcherAdapter watcher = new TextWatcherAdapter() {
      public void onTextChanged(CharSequence s, 
          int start, int before, int count) {
        bindableString.set(s.toString());
      }
    };
    view.setTag(R.id.bound_observable, 
       new Pair<>(bindableString, watcher));
    view.addTextChangedListener(watcher);
  }  
  String newValue = bindableString.get();
  if (!view.getText().toString().equals(newValue)) {
    view.setText(newValue);
  }
}

当然,google不可能真的这么蠢。。让我们自己去实现这一套,它在xml里给我们提供了很简单的运算符@=

@=

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="textNoSuggestions"
    android:text="@={model.name}"/>

这么搞完当EditText更新时就自动更新model.name字段。当然,肯定很好奇背后的实现(这一part看了我半天。。) 它的实现是由多个注解完成的,先看其中两个:

  • @InverseBindingAdapter
  • @InverseBindingListener

TextViewBindingAdapter.java:

@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
            "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
        final OnTextChanged on, final AfterTextChanged after,
        final InverseBindingListener textAttrChanged) {
    final TextWatcher newValue;
    if (before == null && after == null && on == null && textAttrChanged == null) {
        newValue = null;
    } else {
        newValue = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                if (before != null) {
                    before.beforeTextChanged(s, start, count, after);
                }
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (on != null) {
                    on.onTextChanged(s, start, before, count);
                }
                if (textAttrChanged != null) {
                    textAttrChanged.onChange();
                }
            }

            @Override
            public void afterTextChanged(Editable s) {
                if (after != null) {
                    after.afterTextChanged(s);
                }
            }
        };
    }
    final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
    if (oldValue != null) {
        view.removeTextChangedListener(oldValue);
    }
    if (newValue != null) {
        view.addTextChangedListener(newValue);
    }
}
    
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
    return view.getText().toString();
}

xxxViewBinding.java

  private android.databinding.InverseBindingListener mboundView2androidTextAttrChanged = new android.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
            // Inverse of model.str.get()
            //         is model.str.set((java.lang.String) callbackArg_0)
            java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView2);
            // localize variables for thread safety
            // model
            me.lizz0y.myapplication.VM model = mModel;
            // model.str != null
            boolean modelStrJavaLangObjectNull = false;
            // model != null
            boolean modelJavaLangObjectNull = false;
            // model.str.get()
            java.lang.String modelStrGet = null;
            // model.str
            android.databinding.ObservableField<java.lang.String> modelStr = null;
            modelJavaLangObjectNull = (model) != (null);
            if (modelJavaLangObjectNull) {
                modelStr = model.str;
                modelStrJavaLangObjectNull = (modelStr) != (null);
                if (modelStrJavaLangObjectNull) {
                    modelStr.set(((java.lang.String) (callbackArg_0)));
                }
            }
        }
    };

也很简单,当text发生变化时,触发onTextChange,然后调用mboundView2androidTextAttrChanged.onChange,里面调用了由@InverseBindingAdapter注解的getTextString去获取值赋给model

总结一下,假设你要给一个自定义属性双向绑定,写上@=时:你需要写以下函数:

@InverseBindingAdapter(attribute = "refreshing", event = "refreshingAttrChanged")
public static boolean getRefreshing(PhilView view) { //赋值时来这里取
    return isRefreshing;
}

@BindingAdapter(value = {"refreshingAttrChanged"}, requireAll = false)
public static void setRefreshingAttrChanged(PhilView view, final InverseBindingListener inverseBindingListener) {
    Log.d(TAG, "setRefreshingAttrChanged");

    if (inverseBindingListener == null) {
        view.setRefreshingListener(null);
    } else {
        mInverseBindingListener = inverseBindingListener;
        view.setRefreshingListener(mOnRefreshingListener);
    }
}
@InverseMethod & @InverseBindingMethod[s]

参考这篇

只是简化了一下@InverseBindingAdapter的注解。

与RecyclerView

public class MyViewHolder extends RecyclerView.ViewHolder {
    private final ItemBinding binding;

    public MyViewHolder(ItemBinding binding) {
        super(binding.getRoot());
        this.binding = binding;
    }

    public void bind(Item item) {
        binding.setItem(item);
        binding.executePendingBindings();
    }
}

强刷executePendingBinding

强制绑定操作马上执行,而不是推迟到下一帧刷新时。RecyclerView 会在 onBindViewHolder 之后立即测量 View。如果因为绑定推迟到下一帧绘制时导致错误的数据被绑定到 View 中, View 会被不正确地测量,因此这个 executePendingBindings() 方法非常重要!

todoApp结构

1.png

现在先让我们忘记前面说的一切有关双向绑定的事情。。。来看看google推出的架构LiveData&ViewModel

我们先看平时开发时会有哪些问题:

通常Android系统来管理UI controllers(如Activity、Fragment)的生命周期,由系统响应用户交互或者重建组件,用户无法操控。当组件被销毁并重建后,原来组件相关的数据也会丢失,如果数据类型比较简单,同时数据量也不大,可以通过onSaveInstanceState()存储数据,组件重建之后通过onCreate(),从中读取Bundle恢复数据。但如果是大量数据,不方便序列化及反序列化,则上述方法将不适用。

UI controllers经常会发送很多异步请求,有可能会出现UI组件已销毁,而请求还未返回的情况,因此UI controllers需要做额外的工作以防止内存泄露。
当Activity因为配置变化而销毁重建时,一般数据会重新请求,其实这是一种浪费,最好就是能够保留上次的数据。

解决fragmentfragment之间通信的问题

LiveData

LiveData,顾名思义和生命周期绑定,解决上面的第二个异步问题:

  • 能够感知组件(Fragment、Activity、Service)的生命周期;

  • 只有在组件出于激活状态(STARTED、RESUMED)才会通知观察者有数据更新;

public class NameViewModel extends ViewModel{
    // Create a LiveData with a String
    private MutableLiveData<String> mCurrentName;
    // Create a LiveData with a String list
    private MutableLiveData<List<String>> mNameListData;

    public MutableLiveData<String> getCurrentName() {
        if (mCurrentName == null) {
            mCurrentName = new MutableLiveData<>();
        }
        return mCurrentName;
    }

    public MutableLiveData<List<String>> getNameList(){
        if (mNameListData == null) {
            mNameListData = new MutableLiveData<>();
        }
        return mNameListData;
    }
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mNameViewModel = ViewModelProviders.of(this).get(NameViewModel.class);
    mNameViewModel.getCurrentName().observe(this,(String name) -> {
        mTvName.setText(name);
        Log.d(TAG, "currentName: " + name);
    }); // 订阅LiveData中当前Name数据变化,以lambda形式定义Observer
    mNameViewModel.getNameList().observe(this, (List<String> nameList) -> {
        for (String item : nameList) {
            Log.d(TAG, "name: " + item);
        }
    }); // 订阅LiveData中Name列表数据变化,以lambda形式定义Observer
}

当组件处于激活状态,并且mCurrentName变量发生变化时,fragment观察者就会收到监听

ViewModel

[站外图片上传中...(image-93bfcd-1526989876647)]

说实话一开始看到这个我总以为跟MVVMViewModel有什么异曲同工之妙,事实证明我想多了。此ViewModel是用来存储和管理UI相关的数据。

ViewModel是生存在整个生命周期内的,所以在这个类中不能存在android.content.Context; 简单来说不能有viewcontext的引用,所以一般都会传ApplicationContext进去。

用法很简单:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

很容易看出它为什么可以解决以上问题, ViewModelProviders.of(this).get(MyViewModel.class);调用这个时,内部代码会帮我们做存储。至于怎么做存储,也很常见,加了一个fragmentsetRetain(true)就可以了。这样重建恢复后拿的还是同一个ViewModel,因此显然数据也都还在。同时,一个Activity对应的两个fragemt也可以通信:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}


public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

使用getActivity拿到同一份ViewModel,就可以拿到同一份数据。也很容易看到,其实LiveData是需要与ViewModel结合在一起用的

todoApp-mvvm-live

todoApp可以看出最明显的区别:

@NonNull
public static TaskDetailViewModel obtainViewModel(FragmentActivity activity) {
    // Use a Factory to inject dependencies into the ViewModel
    ViewModelFactory factory = ViewModelFactory.getInstance(activity.getApplication());

    return ViewModelProviders.of(activity, factory).get(TaskDetailViewModel.class);
}

其次,其实这个例子还是跟dataBinding搞在一起了,否则拿回数据更新UI时需要用到大量LiveDataobserve函数。

最后所以用了LiveData的作用在哪,我们看一个例子,点击某个按钮后跳转Activity:

before


public class TasksActivity extends AppCompatActivity implements TaskItemNavigator, TasksNavigator {

    ....
    
}


@Nullable
private WeakReference<TaskItemNavigator> mNavigator;


ViewModel中:

public void taskClicked() {
    String taskId = getTaskId();
    if (taskId == null) {
        // Click happened before task was loaded, no-op.
        return;
    }
    if (mNavigator != null && mNavigator.get() != null) { //烦躁
        mNavigator.get().openTaskDetails(taskId);
    }
}

after

TasksActivity.java

// Subscribe to "open task" event
mViewModel.getOpenTaskEvent().observe(this, new Observer<String>() {
    @Override
    public void onChanged(@Nullable String taskId) {
        if (taskId != null) {
            openTaskDetails(taskId);
        }
    }
});

// mTasksViewModel.getOpenTaskEvent().setValue(task.getId());

利用liveData的生命周期特性,就不用管activity是否已经消失。

调试

Databinding想调试自动生成的代码,需要在setting里选择Reference code generated by the compiler

一些额外的技巧

image

image

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

推荐阅读更多精彩内容