Data Binding 數據綁定(一)


前幾天在忙一些其他的東西,DataBinding 這個系列的博客本應該在五月月初就要寫的,結果一直拖到了現在,罪過罪過。
在學習 DataBinding 的過程中,參考 Google 官方的 DataBinding 示例 Demo,自己寫了一個 DataBindingPractice Demo,用于練手。整個工程采用 MVP 架構 + DataBinding,歡迎 star、fork 和溝通交流。
本文介紹了 Data Binding 的一些基本概念和基本用法,主要包括以下四部分內容:

  1. Data Binding 的介紹
  2. Data Binding 中的布局文件
  3. Data Binding 中的事件處理
  4. Data Binding 中的布局詳情

Data Binding 的介紹

簡介

在 Data Binding 庫之前,我們經常會寫一些重復性很高而且毫無營養的代碼,比如:findViewById()setText()setOnClickListener() 等。使用 Data Binding 庫以后,可以使用聲明式布局文件來減少粘結業務邏輯和布局文件的膠水代碼。

  1. Data Binding 具有良好的靈活性和兼容性,它是一個 support 庫,向后兼容至 Android 2.1(API Level 7+)。
  2. 若使用 Android Studio 開發環境開發 Android 應用程序,則必須滿足以下兩個條件才可以使用 Data Binding 庫:
* Gradle Plugin 版本必須是 1.5.0-alpha1 或以上的版本
* Android Studio 的版本必須是1.3或以上的版本。

Data Binding 環境構建

在 Module 的 build.gradle 中添加如下代碼,這樣應用就支持 Data Binding 庫了。

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

注意:若 app Module 依賴了一個使用 Data Binding 的庫,則 app Module 的 build.gradle 也必須配置 Data Binding 庫。

Data Binding 中的布局文件

第一個 data binding 表達式

與傳統的布局文件相比,data binding 布局文件與其只有輕微的不同,data binding 布局文件中的根元素是 <layout> 標簽,其中包含一個 <data> 標簽和一個 <view> 標簽,這個 <view> 標簽的內容與普通布局文件的內容相同。如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>
  1. 在 `` 標簽中的 user 變量是一個在這個 data binding 布局文件中會用到的屬性。
  2. 在 data binding 布局文件中,data binding 表達式使用 @{} 語法。

數據對象(Data Object)

  1. 假設有一個 User 類是 plain-old Java object (POJO) 類型的,如下所示:
public class User {
       public final String firstName;
       public final String lastName;
       public User(String firstName, String lastName) {
           this.firstName = firstName;
           this.lastName = lastName;
       }
 }
  1. 還有一個 User 類,是 JavaBeans 類型的,如下所示:
public class User {
       private final String firstName;
       private final 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;
       }
}
  1. 這兩個 User 類對于 Data Binding 庫來說是等價的。在 TextView 中的 android:text 屬性 @{user.firstName} 會使用 POJO User類中的 firstName 字段,或者 JavaBeans User 類中的 getFirstName() 方法。

綁定對象(Binding Data)

  1. 默認情況下,基于 data binding 布局文件會生成一個 Binding 類,此 Binding 類是將布局文件的名稱轉換成帕斯卡命名,并在之后接上 Binding 命名的。比如,布局文件名稱是 activity_main.xml,則其對應的 Binding 類是 ActivityMainBinding。這個 Binding 類包含了布局文件中所有的布局屬性和布局視圖的綁定關系,并且知道如何向 data binding 表達式賦值。在 inflate 的時候,是創建 binding 關系最簡單的時候,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
       // 下面這行生成 Binding 類的代碼和上面這行生成 Binding 類的代碼是等價的。
       // MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
       User user = new User("Test", "User");
       binding.setUser(user);
}
  1. 如果在 ListView 或者 RecyclerView 中的 Item 中使用 Data Binding,可以使用如下方式生成每個 Item 對應的 Binding 類,如下所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

Data Binding 中的事件處理

Data Binding 庫允許使用 data binding 表達式處理由 View 分發的事件。事件屬性的名字由 Listener 中的方法名稱決定。例如,在 View.onLongClickListener() 中有一個 onLongClick() 方法,則這個事件對應的屬性名稱是 android:onLongClick。有兩種方法處理一個事件:

  • 方法引用:在表達式中,可以引用符合監聽器方法簽名的處理方法。
  • 監聽綁定:在表達式中,是使用一個 Lambda 表達式處理事件的。

兩者的區別,官方說法:方法引用和監聽綁定的主要區別是,方法引用中監聽器的實現是在數據綁定期間完成的,而不是在觸發事件時創建的。如果更偏向于在事件發生時再計算表達式的值,則應該使用監聽綁定。可以理解為:方法引用是在編譯期處理,而監聽綁定是在事件分發時處理。

方法引用(Method References)

  1. 在方法引用中,可以直接將事件綁定到一個處理類的方法上去,類似于 android:onClick 可以指定到一個 Activity 中的方法。和 View.onClick 相比,方法引用表達式的一個主要優點是:方法引用是在編譯期處理的,所以如果引用的方法不存在或者方法的簽名不匹配的話,在編譯期就會報錯。
  2. 若要將一個事件指派給一個處理類,則需要使用一個正常的 data binding 表達式,這個 data binding 表達式的值是將要調用的方法的名稱。例如,有一個類如下所示:
public class MyHandlers {
        public void onClickFriend(View view) { ... }
}

data binding 表達式可以為 View 指定點擊監聽器,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
      <variable name="handlers" type="com.example.MyHandlers"/>
      <variable name="user" type="com.example.User"/>
  </data>
  <LinearLayout
      android:orientation="vertical"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      <TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"
          android:onClick="@{handlers::onClickFriend}"/>
  </LinearLayout>
</layout>

注意:在 data binding 表達式中的方法簽名必須和監聽器對象中的方法簽名匹配,引用的方法的參數必須事件監聽器的方法的參數匹配。

監聽綁定(Listener Bindings)

監聽綁定是在事件發生時才會運行的 data binding 表達式。

  • 監聽綁定在 Gradle Plugin 2.0及更新的版本上才可以使用
  • 和方法引用類似,不過它允許你運行任意數據綁定表達式(不限制處理方法的參數)
  • 在監聽綁定中,引用的方法的返回值和事件監聽器期望的返回值匹配即可(除非它期望是void的)
  1. 例如,有一個 Presenter 類如下所示:
public class Presenter {
        public void onSaveClick(Task task){}
}

可以將點擊事件綁定到這個 presenter 類上,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="task" type="com.android.example.Task" />
        <variable name="presenter" type="com.android.example.Presenter" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"  
        android:layout_height="match_parent">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> presenter.onSaveClick(task)}" />
    </LinearLayout>
</layout>
  • 監聽器由 Lambda 語句表達,并且只允許作為表達式的根元素使用。
  • 如果在表達式中會使用一個回調,Data Binding會自動的創建必要的監聽器并將其注冊到對應的事件。當該控件的事件發生時,Data Binding會計算表達式的值。
  • 在常規的綁定表達式中,當監聽器的表達式計算式,Data Binding會保證綁定表達式中引用變量的空值安全性和線程安全性。
  • 請注意,在上面的例子中,沒有定義傳遞進 onClick() 中的 View 參數。監聽綁定為監聽器的參數提供了兩種選擇:要么把參數全部寫上,要么把參數全部忽略不寫。如果傾向于寫出全部的參數,則上面的例子該像下面這樣寫:
  android:onClick="@{(view) -> presenter.onSaveClick(task)}" 

如果想在表達式中使用參數,則可以像下面代碼一樣使用:

  public class Presenter {
          public void onSaveClick(View view, Task task){}
  }
android:onClick="@{(view) -> presenter.onSaveClick(view, task)}"
  1. 還可以使用具有多個參數的 Lambda 表達式:
public class Presenter {
        public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox
      android:layout_width="wrap_content"   
      android:layout_height="wrap_content"
      android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
  1. 如果正在監聽的事件函數的返回值是非空的,則綁定表達式的值也必須返回相同類型的值。例如,正在監聽 onLongClick() 事件函數,則綁定表達式需要返回 boolean 型的。如下所示:
public class Presenter {
        public boolean onLongClick(View view, Task task){}
}
 android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
  • 如果由于空對象而無法計算綁定表達式的值,則 Data Binding 返回 Java 中默認的值,例如:引用型對象則返回 null, int 型則返回0,Boolean 型則返回 false 等等。
  • 如果需要使用帶斷言(例如三元表達式)的表達式,則可以使用void作為空操作符號。例如:
  android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"    

避免復雜的監聽

  1. 監聽器表達式是非常強大的,會讓你的代碼非常容易閱讀。
  2. 另一方面,如果監聽器中包含復雜的表達式則會讓布局文件難以閱讀和維護,所以布局文件中的表達式應盡可能簡單,表達式只是調用回調方法,而具體的業務邏輯應該寫在回調方法中。
  3. 有個別點擊事件的監聽器回調函數的方法名稱和 View 的 android:onClick 相同,下面有一些新的屬性名稱,用于避免沖突:
Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

Data Binding 中的布局詳情

導入(Imports)

  1. 在布局文件中的 data 標簽中可以使用 import 標簽導入類,就像在 java 文件中導入類一樣,如下所示:
<data>
        <import type="android.view.View"/>
</data>
<TextView
        android:text="@{user.lastName}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
  1. 當導入的類名沖突時,可以使用 alias 屬性為類起個別名,如下所示:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
        alias="Vista"/>
  • 別名只在此布局文件內有效。導入的類,可以在 data binding 表達式中使用,也可以在申明變量時使用。
  • 目前,在 Android Studio 中,并沒有提供 Data Binding 在布局文件中導入類自動補全的功能。如果在布局文件中使用的類,沒有被導入,編譯可以正常通過,但是運行時會出現問題。可以通過在申明變量時,使用完全限定名類避免這個問題隱患。
  1. 在 data binding 表達式中可以使用導入類的靜態方法和靜態字段,如下所示:
<data>
        <import type="com.example.MyStringUtils"/>
        <variable name="user" type="com.example.User"/>
</data>
    …
    <TextView
        android:text="@{MyStringUtils.capitalize(user.lastName)}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
  1. 和在 Java 中一樣,java.lang.* 包下的類會被自動導入。

變量(Variables)

  1. data 標簽中,可以定義任意數量的變量,每個變量都可以被該布局文件中的任意一個 data binding 表達式使用。如下所示:
  <data>
        <import type="android.graphics.drawable.Drawable"/>
        <variable name="user"  type="com.example.User"/>
        <variable name="image" type="Drawable"/>
        <variable name="note"  type="String"/>
</data>
  1. Data Binding 在編譯期內會對申明的變量檢查類型,如果該變量實現了 Observable 接口,或者是一個 Observable 集合類型的,那它應該在類型上反映出來。若是一個沒有實現 Observable 的基礎類或者基礎接口,則該類不會被觀察。
  2. 自動生成的 binding 類會為每個變量生成對應的 setter 和 getter 方法,在每個變量的 setter 方法被調用之前,該變量將會采用默認值,即:引用型變量默認值是null, int 型變量默認值是0,boolean 型變量的默認值是 false。
  3. Data Binding 會生成一個特殊的名為 context 的變量,以便 data binding 表達式在需要的時候使用,此 context 變量的值其實就是該布局文件中 rootViewgetContext() 的返回值。
  4. 如果在該布局文件中有一個名為 context 的變量,則 Data Binding 生成的 context 將會被覆蓋。

自定義 Binding 類的名稱(Custom Binding Class Names)

  1. Data Binding 會為每一個使用了 Data Binding 的布局文件生成一個對應的 Binding 類,該類的名稱是基于布局文件的名稱的,采用大駝峰命名規則,移除下劃線_,并在最后追加 Binding,這個類會被放在該 Module 包的 databinding 包中。例如:如果一個名為 activity_main.xml 的布局文件使用了 Data Binding 庫,則 Data Binding 庫會自動生成一個名為ActivityMainBinding 的 Binding 類,如果該 Module 的包名為 com.lijiankun24.databindingpractice,則 ActivityMainBinding 類在 com.lijiankun24.databindingpractice.databinding 包下。
  2. 通過修改 data 標簽的 class 屬性,就可以修改 binding 類的名稱和位置。
  • 若像下面這樣:
<data class="ActivityCustomBinding">
  ...
</data>

則該布局文件對應的 Binding 類的名稱是 ActivityCustomBinding,而與該布局文件的名稱無關。

  • 若像下面這樣:
 <data class=".ActivityCustomBinding">
   ...
 </data>

則該布局文件對應的 Binding 類會被放在該 Module 包下,而不是該 Module 的 databinding 包下。

  • 若像下面這樣:
  <data class="com.example.ActivityCustomBinding">
    ...
  </data>

則可以任意地指定該布局文件對應的 Binding 類所在的位置。

Includes 標簽

  1. data 標簽中聲明的變量,可以通過應用命名空間將該變量傳遞到被 include 的布局中。如:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>

如上面代碼所示,可以將在 data 標簽中聲明的 user 變量傳遞到name.xmlcontact.xml 布局文件中,前提是在這兩個布局文件中必須也聲明了 user 變量。

  1. Data Binding 庫并不支持 merge 標簽直接做為其子元素,如下所示的代碼是不允許的。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <merge>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

表達式語法(Expression Language)

通用特性

表達式語言和 Java 表達式有很多相似之處,如下所示:

  • 數學運算符:+ - * / %
  • 字符串連接符:+
  • 邏輯運算符:&& ||
  • 位運算符:& | ^
  • 一元操作符:+ - ! ~
  • 移位運算: >> >>> <<
  • 比較運算符:== > < >= <=
  • 實例判斷:instanceof
  • 組:()
  • Literals - character, String, numeric, null
  • 類型轉換 Cast
  • 方法調用 Method calls
  • 字段存取 Field access
  • 數組存取 Array access: []
  • 三目運算符:?:
    如:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

不支持的操作符

一些在 Java 中的操作符,在 Binding 表達式中不支持,如下:

  • this
  • super
  • new
  • 顯式泛型調用

空合并運算符(Null Coalescing Operator)

空合并運算符(??): 如果左操作數不為空,則選擇左操作數否則選擇右操作數。

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

上面代碼等價于:

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

空指針異常處理(Avoiding NullPointerException)

Data Binding 生成的代碼中會自動檢查 null 并避免空指針異常。例如,在 data binding 表達式 @{user.name} 中,如果 user 變量是 null 的,若 name 是 String 類型的,則將為 user.name 分配其默認值 null;若引用了 user.age,其中 age 是 int 型的,那么它的默認值是0。

集合(Collections)

可以使用 [] 操作符來操作通用的容器類,比如:arrays, lists, sparse lists 和 maps,如:

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List<String>"/>
    <variable name="sparse" type="SparseArray<String>"/>
    <variable name="map" type="Map<String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

字符串語法(String Literals)

  1. 當屬性值使用單引號括起來時,在表達式中需要使用雙引號。
android:text='@{map["firstName"]}'
  1. 屬性值也可以使用雙引號括起來,則表達式中的字符串應該使用 ' 或者后引號 ` ,如:
android:text="@{map[`firstName`}"android:text="@{map['firstName']}"

資源(Resources)

  1. 在表達式中可以使用正常的語法引用資源。
android:padding="@{large ? @dimen/largePadding : @dimen/smallPadding}"
  1. 在字符串格式化和復數形式中可以使用參數,如:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
  1. 當復數形式中有多個參數是,多個參數必須同時傳遞進去,如:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
  1. 一些資源需要在表達式中使用特定引用類型,如:
Type Normal Reference Expression Reference
String[] @array @stringArrayint[]
array @intArrayTypedArray @array
typedArrayAnimator @animator @animatorStateListAnimator
animator @stateListAnimatorcolor int @color
colorColorStateList @color @colorStateList

DataBinding 第一篇文章先介紹這些,如果有什么問題歡迎指出。我的工作郵箱:jiankunli24@gmail.com


參考資料:

DataBInding 官方文檔

深入Android Data Binding(一):使用詳解 -- YamLee

Android Data Binding 系列(一) -- 詳細介紹與使用 -- ConnorLin

DataBinding(一)-初識 -- sakasa(譯)Data Binding 指南 -- 楊輝

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容