Andorid利用编译时注解,使用LiveData+DataBinding+ViewModel+LifeCycle封装MVVM极简框架

未经本人同意不得转载

前言

最近有空研究google发布的jectPack,发现自己所有的MVP架构有点跟不上时代了,就封装了一套极简的MVVM框架。其中使用 LiveData,DataBinding,ViewModel,LifeCycle,并且使用编译时注解,封装了一套仿Butterknife初始化框架,废话不多说,上代码。

怎么使用

在项目build.gradle文件中添加

allprojects {
    repositories {
      
        
        maven { url 'https://jitpack.io' }
    }
}

依赖所有模块
如果使用了kotlin项目,必须添加kapt 'com.github.WenYong1994.WenYApp:wheny_compiler_lib:1.1.4'

dependencies {
    
    ...
    
    implementation 'com.github.WenYong1994:WenYApp:1.1.4'
    annotationProcessor 'com.github.WenYong1994.WenYApp:wheny_compiler_lib:1.1.4'
    kapt 'com.github.WenYong1994.WenYApp:wheny_compiler_lib:1.1.4'  
    
    ...

        
    //需要正常使用的第三方依赖  
        
    //添加lifeCycler
    implementation group: 'androidx.lifecycle', name: 'lifecycle-common-java8', version: '2.2.0'
    implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
    //添加ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0"
    //添加recyclerView
    implementation "com.android.support:recyclerview-v7:${recyclerview_libary_version}"
    //添加网络okhttp3 和RxJava
    implementation "com.squareup.okhttp3:okhttp:4.2.0"
    implementation "io.reactivex.rxjava2:rxjava:2.1.0" // 必要rxjava2依赖
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
    implementation "io.reactivex.rxjava2:rxandroid:2.0.1" // 必要rxandrroid依赖,切线程时需要用到
    implementation 'com.squareup.retrofit2:retrofit:2.3.0' // 必要retrofit依赖
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0' // 必要依赖,解析json字符所用
    implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1' //非必要依赖, log依赖,如果需要打印OkHttpLog需要添加
    //
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' // 必要依赖,和Rxjava结合必须用到,下面会提到   
    
}

注意

如果想使用某一个部分功能请看下面的注释

{
    implementation 'com.github.WenYong1994:WenYApp:1.1.4'//依赖所有模块
//    implementation 'com.github.WenYong1994.WenYApp:whenynetlibrary:1.1.4'//网络相关模块
//
//    implementation 'com.github.WenYong1994.WenYApp:wheny_annotation_apilib:1.1.4'//MVVM相关注解初始化
//    implementation 'com.github.WenYong1994.WenYApp:wheny_annotation_lib:1.1.4'//MVVM相关注解初始化
//
//    implementation 'com.github.WenYong1994.WenYApp:whenydblibary:1.1.4'//数据库相关模块
//
//    implementation 'com.github.WenYong1994.WenYApp:whenylibrary:1.1.4'//工具类相关模块

    annotationProcessor 'com.github.WenYong1994.WenYApp:wheny_compiler_lib:1.1.0'//如果使用到MVVM注解初始化 必须添加  
}

更多使用方式请在文章结尾的传送门去GitHub项目源码查看示例

先来简单介绍一下本框架的每个组件

MVVM

MVVM是一套MVP的升级,将MVP里面的P层替换为VM层,其他层所负责的功能不变化。这里的VM主要是使用了DataBinding来进行数据绑定。并在VM里面处理交互逻辑

LiveData

LiveData是一个可以感知生命周期变化,并且可监听的数据

简单示例

class LoginActivity : AppCompatActivity() {

    var testLive = MutableLiveData<String>("")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var layoutId= R.layout.activity_login
        setContentView(layoutId)
        
        testLive.observe(this, Observer<String> {
            Log.e("testLive",it)
        })
        test_2.setOnClickListener {
            testLive.postValue("test")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
    }

}

这里testLive调用observe方法时会传入一个LifeCycle进去。只有当LifeCycle处于可见状态时Observer才会被调用,并且会储存最后一次数据变化,直到LifeCycle变为可见时Observer会被调用

DataBinding

DataBinding主要是负责数据绑定(缺点,由于使用了伪JAVA代码来绑定数据,出错就很难定位错误的具体位置,但是熟悉之后还是比较好定位问题)就不一 一赘述了
例子:
使用在项目的build.gradle文件里开启 DataBinding

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

新建VM类,TestVm,这里继承BaseObservable是让数据驱动UI更新

import android.view.View
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.databinding.library.baseAdapters.BR

class TestVm : BaseObservable() {

    @Bindable
    var txt1 = "1"
    set(value) {
        field = value
        notifyPropertyChanged(BR.txt1)
    }

    fun onClickTest(view: View){
        txt1 = "test"
    }


}

xml中编写,dataBinding的双向绑定使用 android:text="@={testVm.txt1}"

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".mvvm.view.databinding.TestDataBindingActivity">
    
    <data>
        <variable
            name="testVm"
            type="com.example.wenyapplication.mvvm.view.databinding.TestVm" />
        
    </data>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{testVm.txt1}"
            tools:text="2"
            />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="测试"
            android:onClick="@{(view)->testVm.onClickTest(view)}"
            />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={testVm.txt1}"
            />

    </LinearLayout>
    
    
</layout>

Activity中

class TestDataBindingActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_test_data_binding)
        val contentView = DataBindingUtil.setContentView<ActivityTestDataBindingBinding>(this, R.layout.activity_test_data_binding)
        contentView.testVm = TestVm()

    }
}

更多的自定义双向绑定就请同学自行度娘

ViewModel

ViewModel是google推出的一款为规划MVVM框架所提出的。他主要功能是既充当了MVVM中VM的角色,也有其本来的功能,如生命周期会比Activity长(所以Google不建议 ViewModel持有任何Activity,Fragment,避免内存泄漏,只有纯业务代码,也方便单元测试),当activity被系统销毁时,例如旋转屏幕时,ViewModel会储存数据,数据不会丢失。(有一些极端情况下,ViewModel也会被销毁,这就需要在OnSaveInstance里面来进行存储关键数据),在同一个Activity里面不同地方获取的一个类的ViewModel是单列的。所以可以用于fragment,dialog之间的通信。配合LiveData还可以同步数据变化(并且不用考虑activity的生命周期,LiveData的观察者会自动处理)

简单例子

var mLoginVm: LoginVm?=null

mLoginVm = ViewModelProviders.of(this).get(LoginVm::class.java)

LifeCycle

LifeCycle是一个让其他组件可以轻松的监听activity生命周期

lifecycle.addObserver(object : LifecycleEventObserver{
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                when (event){
                    Lifecycle.Event.ON_START->{

                    }
                    Lifecycle.Event.ON_DESTROY->{

                    }
                }                
            }
        })

重点来了

编译时注解

编译时注解是只会存在于项目编译时,并且项目在编译时会调用自定义Provessor来进行生成中间JAVA类,不清楚的同学可以在网上看看资料

本篇文章的灵魂来了

本着懒得习惯,能少一句代码是一句代码的宗旨,所以使用编译时注解模仿butterKnife的方式对ViewModel和DataBinding的初始化,并且在BaseViewModel中实现了LifeCycle的监听(注意这里不能让ViewModel持有lifecycle,而是让ViewModel实现DefaultLifecycleObserver,在Activity中调用lifecycle.addObserver)。

使用方式

加载wheny_annotation_lib、wheny_annotation_apilib、wheny_compiler_lib,

这里 wheny_annotation_lib 和 wheny_annotation_apilib 是javaLib

这三个model的依赖方式是
wheny_annotation_apilib 依赖 wheny_annotation_lib
wheny_compiler_lib 依赖 wheny_annotation_lib
需要使用的model 依赖 wheny_annotation_lib和wheny_annotation_apilib

具体使用方式,使用注解的方式进行对ViewModel的初始化

class LoginActivity : AppCompatActivity() {

    @InjectViewModel(dataBindFieldName = "LoginVm")
    var mLoginVm: LoginVm?=null
    @InjectViewModel(dataBindFieldName = "LoginVm2",needFactory = true)
    var mLoginVm2: LoginVm2?=null
    @InjectViewModel(needFactory = true)
    var mLoginVm3: LoginVm2?=null

//    var testLive = MutableLiveData<String>("")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var layoutId= R.layout.activity_login
//        setContentView(layoutId)
        val loginBinding = ViewModelInjector.inject<ActivityLoginBinding>(this, layoutId)
        
        ViewModelInjector.injectByFactory(this, LoginVm2Factory("ssss", App.getApplication()),"mLoginVm2",loginBinding)
        ViewModelInjector.injectByFactory(this, LoginVm2Factory("33333", App.getApplication()),"mLoginVm3",loginBinding)

        mLoginVm = ViewModelProviders.of(this).get(LoginVm::class.java)

//        testLive.observe(this, Observer<String> {
//            Log.e("testLive",it)
//        })
//        test_2.setOnClickListener {
//            testLive.postValue("test")
//        }

//        lifecycle.addObserver(object : LifecycleEventObserver{
//            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
//                when (event){
//                    Lifecycle.Event.ON_START->{
//
//                    }
//                    Lifecycle.Event.ON_DESTROY->{
//
//                    }
//                }
//            }
//        })



    }

    override fun onDestroy() {
        super.onDestroy()
    }

}



xml

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".mvvm.view.login.LoginActivity">

    <data>
        <variable
            name="LoginVm"
            type="com.example.wenyapplication.mvvm.view.login.LoginVm" />

        <variable
            name="LoginVm2"
            type="com.example.wenyapplication.mvvm.view.login.LoginVm2" />

    </data>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="账号"
            android:text="@={LoginVm2.account}"
            />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="密码"
            android:text="@={LoginVm2.pwd}"
            />
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{()->LoginVm2.doLogin()}"
            android:text="登录"
            />
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{(view) -> LoginVm2.test()}"
            android:text="test"
            />

        <Button
            android:id="@+id/test_2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="test2"
            />

        <TextView
            android:id="@+id/tvMsg"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{LoginVm2.loginBean.message}"
            />


        <com.example.wenyapplication.view.InputTextLine
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:single_input_title="sassss"
            app:single_input_text="@={LoginVm2.loginBean.message}"
            />

    </LinearLayout>

</layout>

这里LoginVm没有实际意义只是表明 一个Activity可以有多个VM,处理不同的业务逻辑

LoginVm2是带有构造参数的ViewModel。初始化的时候必须使用Factory的方式

mLoginVm2 = LoginVm2Factory("ssss", App.getApplication()).create(LoginVm2::class.java)
loginBinding.loginVm2=mLoginVm2

LoginVm2

class LoginVm2(string:String,app: Application) : BaseAndroidViewModel(app){

    var loginBean = MutableLiveData<LoginBean>()

    var account:String = ""
    var pwd : String =""
    var msg :String = string
        set(value) {
            field = value
        }

    fun doLogin() {
        var json = "{\"channel\":\"android\",\"data\":\"{\\\"password\\\":\\\"123456\\\",\\\"os\\\":\\\"android\\\",\\\"uid\\\":0,\\\"username\\\":\\\"kred001\\\"}\",\"salt\":\"064797\",\"service\":\"functionaryLoginService\",\"test\":true,\"time\":1590024509,\"version\":\"1.0\"}"
        val requestBody = json.toRequestBody("application/json; charset=utf-8;".toMediaTypeOrNull())
        val compose = RetrofitServiceManager.getInstance().create(ApiService::class.java)
                .login(requestBody)
                .flatMap {
                    SystemClock.sleep(2000)
                    Flowable.just(it.data.toString())
                }
                .compose(RxSchedulers.io_main())
                .subscribe({
                    Log.e("doLogin","doLogindoLogindoLogindoLogindoLogindoLogindoLogin");
                    loginBean.value= LoginBean(0, it)
                },{
                    loginBean.value=LoginBean(0,it.message.toString())
                })
        addDisposable(compose)
    }


    fun test(){
        Handler().postDelayed({ loginBean.value?.message?.postValue(loginBean.value?.message?.value+"1") }, 2000)
    }


}


class LoginVm2Factory(private val string:String,private val app: Application) : ViewModelProvider.NewInstanceFactory(){
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return LoginVm2(string, app) as T
    }
}

这里的ViewModelInjector还兼容了在Fragment中的初始化

class TestFragment : Fragment() {

    @InjectViewModel(dataBindFieldName = "testFraVm")
    var testFraVm:TestFraVm?=null
    @InjectViewModel
    var mainVm: MainVm? = null




    var rootView :View?= null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        var dt = ViewModelInjector.inject<FragmentTestBinding>(this,activity as AppCompatActivity,inflater,R.layout.fragment_test,container)
        rootView = dt.root
        testFraVm?.test?.postValue("212112124")
        return rootView
    }

    override fun onPause() {
        super.onPause()
    }

}

fragment的xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".mvvm.view.main.TestFragment">
    
    
    <data>
        <variable
            name="testFraVm"
            type="com.example.wenyapplication.mvvm.view.main.TestFraVm" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="#fa1aaf"
        >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{testFraVm.test}"
            tools:text="ahhhahhahah"
            />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="sfadfsafsafss"
            />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{()-> testFraVm.testCl()}"
            />


    </LinearLayout>

</layout>

MainActivity

public class MainActivity extends AppCompatActivity {



    @InjectViewModel(dataBindFieldName = "mainVm")
    MainVm mainVm;
    @InjectViewModel(dataBindFieldName = "testFraVm")
    TestFraVm testFraVm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewModelInjector.inject(this, R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.main_fra,new TestFragment()).commit();
        getSupportFragmentManager().beginTransaction().add(R.id.main_fra_2,new TestFragment2()).commit();
    }

    public void setMainVm(MainVm mainVm) {
        this.mainVm = mainVm;
    }

    public void setTestFraVm(TestFraVm testFraVm) {
        this.testFraVm = testFraVm;
    }

}

MainActivity的xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".mvvm.view.main.MainActivity"
    >

    <data>

        <variable
            name="mainVm"
            type="com.example.wenyapplication.mvvm.view.main.MainVm" />

        <variable
            name="testFraVm"
            type="com.example.wenyapplication.mvvm.view.main.TestFraVm" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >

        <Button
            android:id="@+id/btn_databinding"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{mainVm.dataBindingBtnTxt}"
            android:onClick="@{(view) -> mainVm.testDataBinding(view)}"
            />

        <Button
            android:id="@+id/btn_mvvm"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{mainVm.loginVmTxt}"
            android:onClick="@{(view) -> mainVm.testLogin(view)}"
            />

        <Button
            android:id="@+id/test_net"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{mainVm.testNetTxt}"
            />

        <Button
            android:id="@+id/test_list"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{mainVm.testListTxt}"
            android:onClick="@{(view) -> mainVm.testList(view)}"
            />

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



        <FrameLayout
            android:id="@+id/main_fra"
            android:layout_width="match_parent"
            android:layout_height="200dp"/>




        <FrameLayout
            android:id="@+id/main_fra_2"
            android:layout_width="match_parent"
            android:layout_height="200dp"/>

    </LinearLayout>


</layout>

重点 这里activity中的mainVm和fragment的mainVm是同一个对象,所以轻松实现fragment和activity的通信

这里在Activity中使用方式是

这里使用注解来注解变量,
dataBindFieldName 指的是在layout里面 variable的name标签,如果不填写就不会将ViewModel绑定到ViewDataBinding中
needFactory指的 是否需要使用Factory来创建ViewModel

@InjectViewModel(dataBindFieldName = "mainVm")
MainVm mainVm;



注意这里如果使用Factory来创建ViewModel,在同一个activity中同一个ViewModel类就不会是同一个对象

@InjectViewModel(dataBindFieldName = "LoginVm")
var mLoginVm: LoginVm?=null
@InjectViewModel(dataBindFieldName = "LoginVm2",needFactory = true)
var mLoginVm2: LoginVm2?=null
@InjectViewModel(needFactory = true)
var mLoginVm3: LoginVm2?=null

ViewModelInjector.injectByFactory(this, LoginVm2Factory("ssss", App.getApplication()),"mLoginVm2",loginBinding)
ViewModelInjector.injectByFactory(this, LoginVm2Factory("33333", App.getApplication()),"mLoginVm3",loginBinding)

这里是使用Factory来初始化ViewModel,注意这里的mLoginVm2,和mLoginVm3 是被注解的成员变量的变量名

否则成员变量在执行之后不会被赋值

如果是java语法,必须实现这个被注解的变量的set方法

 public void setMainVm(MainVm mainVm) {
        this.mainVm = mainVm;
    }

最后大家如果想看看源码的可以直接去github源码
欢迎大家指出错误和改进

另外此项目我会持续更新

项目地址

谢谢大家观看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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