Android DataBinding 基本用法

本篇文章以這個示例BasicSample來研究學習Android DataBinding的基本用法株汉。

開始使用

首先請在應(yīng)用模塊的 build.gradle 文件中添加 dataBinding 元素筐乳,如下所示

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

然后將常規(guī)的布局文件轉(zhuǎn)化為數(shù)據(jù)綁定布局文件

<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">

    <data>
      
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
  1. 使用<layout>標簽包裹常規(guī)的布局文件
  2. 添加<data>標簽,在<data>標簽內(nèi)添加布局變量和布局表達式(不是必須的)

可以使用Android Studio的快捷功能將普通布局轉(zhuǎn)化為數(shù)據(jù)綁定布局


convert_layout.png

將鼠標懸停在布局文件的根元素上乔妈,點擊左側(cè)出現(xiàn)的黃色小燈哥童,然后選擇Convert to Data binding layout

BasicSample如下所示

screenshotbasic.png

本篇文章主要學習一下幾點

  1. 布局變量和布局表達式
  2. 觀察能力褒翰,通過可觀察的變量贮懈,LiveData 和可觀察的類實現(xiàn)觀察能力
  3. 綁定適配器,綁定方法和綁定轉(zhuǎn)換器
  4. 和ViewModels無縫結(jié)合

布局變量和布局表達式

使用布局變量和布局表達式可以少寫很多樣板代碼和重復(fù)代碼优训。布局變量和布局表達式將一些UI操作從activities and fragments移到XML布局文件中朵你。

例如,我們要給一個TextView動態(tài)設(shè)置text揣非,我們會在activity中這樣寫

TextView textView = findViewById(R.id.name);
textView.setText(user.name);

但是使用數(shù)據(jù)綁定抡医,我們可以直接在XML布局文件中直接將布局變量賦值給TextView的text屬性

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

具體實現(xiàn)
首先定義我們用到的數(shù)據(jù)類

data class ObservableFieldProfile(
        val name: String,
        val lastName: String,
        val likes: ObservableInt
)

修改布局文件

<?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">

    <data>

        <!--注釋1處-->
        <variable
            name="user"
            type="com.example.android.databinding.basicsample.data.ObservableFieldProfile" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.example.android.databinding.basicsample.ui.BlogDemoActivity">

        <TextView
            android:id="@+id/tvName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

在注釋1處,我們在data標簽內(nèi)定義了user變量早敬,類型是ObservableFieldProfile忌傻,然后我們給TextView的屬性賦值為user.name

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

修改布局文件以后,我們點擊一下Make project搞监,數(shù)據(jù)綁定框架會為我們生成一些用用的類水孩。

然后我們還要修改Activity,如下所示

class BlogDemoActivity : AppCompatActivity() {
    
    //定義我們的數(shù)據(jù)
    private val fieldProfile = ObservableFieldProfile("Ada", "Lovelace",
            ObservableInt(0))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //注釋1處琐驴,將Activity和布局文件綁定
        val binding: ActivityBlogDemoBinding = DataBindingUtil
                .setContentView(this, R.layout.activity_blog_demo)
        //注釋2處俘种,為布局變量user賦值
        binding.user = fieldProfile
    }
}

這個ActivityBlogDemoBinding類,就是數(shù)據(jù)綁定框架幫我們生成的中間類绝淡,我們看見生成的類名和我們的布局文件名字是對應(yīng)的宙刘。布局文件名是activity_blog_demo,生成的綁定類名是ActivityBlogDemoBinding牢酵⌒可以看到就是把布局文件名轉(zhuǎn)成駝峰命名然后在后面加上Binding。

我們也可以自定義生成的綁定類名馍乙,如下所示:

    <data class="ActivityMyBlogDemoBinding">

    </data>

我們運行一下可以看到TextView的text顯示是Ada布近。

觀察能力

當數(shù)據(jù)改變的時候為了能自動實現(xiàn)UI更新垫释,需要將可觀察的對象和View的屬性綁定。有三種機制可以實現(xiàn)這個目標:可觀察的變量吊输,LiveData 和可觀察的類饶号。

可觀察的變量(Observable fields)

數(shù)據(jù)綁定框架提供了像ObservableInt铁追,ObservableBoolean來替代原始數(shù)據(jù)類型季蚂,使其具備可觀察能力。提供了ObservableField來替代引用數(shù)據(jù)類型琅束,使其具備可觀察能力扭屁。
我們在上面定義的ObservableFieldProfile類啥寇,它的likes就是ObservableInt類型的音五。
我們將likes綁定到TextView的text屬性,然后手動改變likes讳推,我們觀察TextView的text是否也跟著改變艾船。

<TextView
            android:id="@+id/tvLikes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{Integer.toString(user.likes)}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btnChangeLike"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Change like"
            android:textAllCaps="false" />

這里有一點要注意葵腹,

 android:text="@{Integer.toString(user.likes)}"

因為likes就是ObservableInt類型,我們使用的時候會自動轉(zhuǎn)化成int類型屿岂,將likes賦值給text屬性的時候践宴,需要將likes轉(zhuǎn)化成字符串類型。這里我們可以看到爷怀,我們可以直接在布局文件中不需要導(dǎo)入就可以使用一些常見的類阻肩,Integer,String运授,等等烤惊,具體哪些類有待完善。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //注釋1處吁朦,將Activity和布局文件綁定
        val binding: ActivityBlogDemoBinding = DataBindingUtil
                .setContentView(this, R.layout.activity_blog_demo)

        binding.btnChangeLike.setOnClickListener {
            //增加likes
            fieldProfile.likes.set(fieldProfile.likes.get() + 1)
        }
    }

我們點擊按鈕發(fā)現(xiàn)柒室,likes增加的時候,TextView的text也會跟著變逗宜。

LiveData

LiveData是Android Architecture Components 中的一個可觀察者具有生命周期感知能力伦泥。和可觀察的變量相比,LiveData的優(yōu)勢是支持 Transformations并且能和其他組件和庫一起配合使用锦溪,例如Room和WorkManager不脯。

我們定義一個類ProfileLiveDataViewModel。ViewModel是一個用來為Activity或者Fragment準備和管理數(shù)據(jù)的類刻诊。ViewModel可以通過LiveData來提供數(shù)據(jù)防楷。關(guān)于ViewModel和LiveData可以參考ViewModel和LiveData 使用

class ProfileLiveDataViewModel : ViewModel() {
    private val _likes =  MutableLiveData(0)
    val likes: LiveData<Int> = _likes //暴露一個不可更改的LiveData

    //改變_likes
    fun onLike() {
        _likes.value = (_likes.value ?: 0) + 1
    }
}

看看布局文件的改變则涯,只保留了關(guān)鍵信息

    <data>

        <!--注釋1處-->
        <variable
            name="viewmodel"
            type="com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel"/>

    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
       tools:context="com.example.android.databinding.basicsample.BlogDemo2Activity">

        <TextView
            android:id="@+id/tvLikes"
            android:text="@{Integer.toString(viewmodel.likes)}"
        />

    </androidx.constraintlayout.widget.ConstraintLayout>

聲明了布局變量viewmodel复局,類型是ProfileLiveDataViewModel冲簿。將viewmodel的likes賦值給TextView的text屬性。

Activity的改變


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityBlogDemo2Binding>(this, R.layout.activity_blog_demo2)
        //獲取viewModel對象
        val viewModel = ViewModelProvider(this).get(ProfileLiveDataViewModel::class.java)
        // 賦值給布局變量
        binding.viewmodel = viewModel

        //注釋1處亿昏,
        //給binding設(shè)置生命周期持有者
        //LiveData需要生命周期持有者峦剔,因為LiveData數(shù)據(jù)改變的時候只會通知生命周期處于活動狀態(tài)的觀察者
        binding.lifecycleOwner = this

        btnChangeLike.setOnClickListener {
            viewModel.onLike()
        }
    }
//...

獲取viewmodel賦值給布局變量,注釋1處角钩,我們要給binding設(shè)置生命周期持有者吝沫。LiveData需要生命周期持有者,因為LiveData數(shù)據(jù)改變的時候只會通知生命周期處于活動狀態(tài)的觀察者递礼。

點擊btnChangeLike也是可以改變TextView的text惨险。

可觀察的類(Observable classes)

為了更加靈活可控,你可以實現(xiàn)一個完整的可觀察的類來決定何時更新哪些變量脊髓。這允許你創(chuàng)建變量之間的依賴關(guān)系并且只更新部分UI辫愉。

/**
 * A ViewModel that is also an Observable, to be used with Data Binding.
 */
open class ObservableViewModel : ViewModel(), Observable {
    
    //屬性改變注冊器
    private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
    
    //添加觀察者
    override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
        callbacks.add(callback)
    }
    //移除觀察者
    override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
        callbacks.remove(callback)
    }

    /**
     * 通知觀察者,當前對象的所有屬性發(fā)生了該變将硝。
     */
    fun notifyChange() {
        callbacks.notifyCallbacks(this, 0, null)
    }

    /**
     * 通知觀察者指定的屬性發(fā)生了改變恭朗。
     * 改變的屬性的getter方法應(yīng)該使用Bindable來注解,用于在`BR`類中生成一個field依疼。
     *
     * @param fieldId 為可綁定的屬性在BR類中生成的field
     */
    fun notifyPropertyChanged(fieldId: Int) {
        callbacks.notifyCallbacks(this, fieldId, null)
    }
}

我們自定義了一個ObservableViewModel類并且繼承了Observable痰腮。所以O(shè)bservableViewModel是一個可觀察者。我們還定義了添加涛贯、移除和通知觀察者的方法诽嘉。

要注意的是:改變的屬性的getter方法應(yīng)該使用Bindable來注解,用于在BR類中生成一個field弟翘,如下所示虫腋。

//繼承ObservableViewModel類
class ProfileObservableViewModel : ObservableViewModel() {
    val likes = ObservableInt(0)

    fun onLike() {
        likes.increment()
        notifyPropertyChanged(BR.popularity)
    }
    
    //注釋1處
    @Bindable
    fun getPopularity(): Popularity {
        return likes.get().let {
            when {
                it > 9 -> Popularity.STAR
                it > 4 -> Popularity.POPULAR
                else -> Popularity.NORMAL
            }
        }
    }
}
//流行度
enum class Popularity {
    NORMAL,//正常
    POPULAR,//流行
    STAR//明星
}
//定義ObservableInt的擴展函數(shù)
private fun ObservableInt.increment() {
    set(get() + 1)
}

在注釋1處,使用Bindable注解getPopularity方法稀余。

@Bindable
    fun getPopularity(): Popularity {
}

點擊Make project悦冀,可以在生成的BR類中看到popularity的field

public class BR {
  //...
  public static final int popularity = 2;
}

在上面的例子中睛琳,當onLike方法被調(diào)用的時候盒蟆,likes會增加并且popularity屬性也會收到數(shù)據(jù)改變的通知(popularity的值依賴likes)。getPopularity方法會被數(shù)據(jù)綁定框架調(diào)用师骗,返回一個可能發(fā)生了變化的新值历等。

Bindable注解應(yīng)該被應(yīng)用到Observable類中的所有g(shù)etter方法上。Bindable會在BR類中生成一個field來標識Observable類中發(fā)生改變的屬性辟癌。

綁定適配器寒屯,綁定方法和綁定轉(zhuǎn)換器

綁定適配器(Binding adapters)

綁定適配器可以用來自定義布局屬性。例如你可以為ProgressBar定義app:progressTint屬性,根據(jù)外部的值來改變進度條的顏色寡夹。

@BindingAdapter("app:progressTint")
@JvmStatic fun tintPopularity(view: ProgressBar, popularity: Popularity) {

    val color = getAssociatedColor(popularity, view.context)
    view.progressTintList = ColorStateList.valueOf(color)
}

//根據(jù)流行度返回不同的顏色
private fun getAssociatedColor(popularity: Popularity, context: Context): Int {
    return when (popularity) {
        Popularity.NORMAL -> context.theme.obtainStyledAttributes(
                    intArrayOf(android.R.attr.colorForeground)).getColor(0, 0x000000)
        Popularity.POPULAR -> ContextCompat.getColor(context, R.color.popular)
        Popularity.STAR -> ContextCompat.getColor(context, R.color.star)
    }
}

在布局文件中使用

<ProgressBar
        app:progressTint="@{viewmodel.popularity}" />

使用綁定適配器可以將在activity中的UI調(diào)用移到靜態(tài)方法中处面,利于封裝。

你也可以在綁定適配器中使用多個屬性菩掏。

@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
@JvmStatic
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
    progressBar.progress = (likes * max / 5).coerceAtMost(max)
}

在這個綁定適配器方法中魂角,要求傳入兩個參數(shù),likes 智绸,max野揪,對應(yīng)布局文件中的"app:progressScaled"屬性和"android:max"屬性。

<ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyleHorizontal"
            android:max="@{100}"
            app:hideIfZero="@{viewmodel.likes}"
            app:progressScaled="@{viewmodel.likes}"
            app:progressTint="@{viewmodel.popularity}"
            tools:progressBackgroundTint="@android:color/darker_gray" />

綁定方法和綁定轉(zhuǎn)換器(Binding methods and binding converters)

當綁定適配器方法很簡單的時候传于,你可以使用綁定方法和綁定轉(zhuǎn)換器來減少代碼囱挑。你可以在 official guide中查閱細節(jié)醉顽。

例如沼溜,當一個屬性值需要傳遞給一個適配器方法:

@BindingAdapter("app:srcCompat")
@JvmStatic fun srcCompat(view: ImageView, @DrawableRes drawableId: Int) {
    view.setImageResource(drawable)
}

上面的適配器方法可以使用綁定方法替代。綁定方法可以被添加到工程中的任意的類中游添。

@BindingMethods(
        BindingMethod(type = ImageView::class,
                attribute = "app:srcCompat",
                method = "setImageResource"))
class MyBindingMethods

我們將這個綁定方法添加到MyBindingMethods上系草。BindingMethod注解的三個屬性我們也留意一下:

  • type = ImageView::class,屬性關(guān)聯(lián)的View類
  • attribute = "app:srcCompat"唆涝,屬性名稱找都,所有的android屬性使用android:命名空間,應(yīng)用自定義屬性不用使用命名空間廊酣。
  • method = "setImageResource"能耻,被數(shù)據(jù)綁定框架調(diào)用,用來設(shè)置屬性值的方法亡驰。

綁定轉(zhuǎn)換器(使用的時候要小心)

在這個例子中晓猛,我們展示了一個View依賴一個數(shù)字是否為0來決定View是否可見。有很多方法來實現(xiàn)這個功能凡辱。本例會展示兩種實現(xiàn)方式戒职。

我們的目標是根據(jù)likes的值來控制View的顯示或者隱藏。

android:visibility="@{viewmodel.likes}"

這種方式是不能正常工作的透乾。likes是一個整數(shù)洪燥,View的visibility屬性也是一個整數(shù) (VISIBLE, GONE and INVISIBLE are 0, 4 and 8 respectively)。所以直接這樣使用的話乳乌, 可以編譯運行捧韵,但是結(jié)果肯定不是我們想要的。

一個可能的解決方式如下:

android:visibility="@{viewmodel.likes == 0 ? View.GONE : View.VISIBLE}"

布局表達式汉操,增加復(fù)雜度不利于閱讀和維護再来,不推薦使用

第一種實現(xiàn)方式客情,就是使用綁定轉(zhuǎn)換器(不推薦的方法)

object ConverterUtil {
    @JvmStatic fun isZero(number: Int): Boolean {
        return number == 0
    }
}

我們首先在ConverterUtil類中定義一個isZero方法其弊,然后在布局文件中導(dǎo)入ConverterUtil類

<data>
        <import type="com.example.android.databinding.basicsample.util.ConverterUtil" />
        ...
</data>

//使用
android:visibility="@{ConverterUtil.isZero(viewmodel.likes)}"

這樣就行了癞己?顯然不可以,ConverterUtil的isZero方法返回值是一個boolean類型梭伐。而android:visibility接收一個Integer類型痹雅。所以我們還需要定義一個綁定轉(zhuǎn)換器方法,將這個boolean類型值轉(zhuǎn)化為Integer類型糊识。如下所示:

object BindingConverters{

    //BindingConversion使用注解
    @BindingConversion
    @JvmStatic fun booleanToVisibility(isNotVisible: Boolean): Int {
        return if (isNotVisible) View.GONE else View.VISIBLE
    }
}

android:visibility接收了一個boolean類型參數(shù)以后绩社,就會去找是否有可以把boolean類型值轉(zhuǎn)化為Integer類型的綁定方法,如果找到了赂苗,就會進行轉(zhuǎn)換愉耙。在編譯期間,編譯器會為我們做檢查拌滋,如果找不到這樣的方法就會報錯朴沿,編譯不過。

注意:這種轉(zhuǎn)換是不安全的败砂,因為我們無法將其限制在我們這一種場景下赌渣。如果一個View屬性接收Integer類型,然后我們給該View傳遞了一個boolean類型的值昌犹,那么它會將該boolean值轉(zhuǎn)化為View可見性的Integer值坚芜。在這個例子中就是View.GONE(8)或者View.VISIBLE(0)。

推薦的方法斜姥,使用綁定適配器

@BindingAdapter("app:hideIfZero")  // Recommended solution
@JvmStatic fun hideIfZero(view: View, number: Int) {
        view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
app:hideIfZero="@{viewmodel.likes}"

這種通過自定義一個新的屬性的方式鸿竖,可以避免被意外使用。

根據(jù)經(jīng)驗铸敏,使用綁定適配器創(chuàng)建自己的自定義屬性比在布局表達式中增加邏輯更好缚忧,也比綁定轉(zhuǎn)換器更安全,推薦使用搞坝。

和ViewModels無縫結(jié)合

上面在說LiveData的時候提到了搔谴,這里就不說了。

完整代碼請參考官方示例BasicSample

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桩撮,一起剝皮案震驚了整個濱河市敦第,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌店量,老刑警劉巖芜果,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異融师,居然都是意外死亡右钾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舀射,“玉大人窘茁,你說我怎么就攤上這事〈嘌蹋” “怎么了山林?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長邢羔。 經(jīng)常有香客問我驼抹,道長,這世上最難降的妖魔是什么拜鹤? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任框冀,我火速辦了婚禮,結(jié)果婚禮上敏簿,老公的妹妹穿的比我還像新娘明也。我一直安慰自己,他們只是感情好极谊,可當我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布诡右。 她就那樣靜靜地躺著安岂,像睡著了一般轻猖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上域那,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天咙边,我揣著相機與錄音,去河邊找鬼次员。 笑死败许,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的淑蔚。 我是一名探鬼主播市殷,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼刹衫!你這毒婦竟也來了醋寝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤带迟,失蹤者是張志新(化名)和其女友劉穎音羞,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仓犬,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡嗅绰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窘面。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡翠语,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出财边,到底是詐尸還是另有隱情啡专,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布制圈,位于F島的核電站们童,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鲸鹦。R本人自食惡果不足惜慧库,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望馋嗜。 院中可真熱鬧齐板,春花似錦、人聲如沸葛菇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眯停。三九已至济舆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間莺债,已是汗流浹背滋觉。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留齐邦,地道東北人椎侠。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像措拇,于是被迫代替她去往敵國和親我纪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容