Flutter入門三部曲(3) - 數(shù)據(jù)傳遞/狀態(tài)管理

Flutter數(shù)據(jù)傳遞
分為兩種方式。一種是沿著數(shù)的方向從上向下傳遞狀態(tài)探橱。另一種是 從下往上傳遞狀態(tài)值资柔。

沿著樹的方向,從上向下傳遞數(shù)據(jù)淑际、狀態(tài)

按照Widgets Tree的方向畏纲,從上往子樹和節(jié)點上傳遞狀態(tài)扇住。

InheritedWidget & ValueNotifier

InheritedWidget

這個既熟悉又陌生類可以幫助我們在Flutter中沿著樹向下傳遞信息。這個類只是簡單的保存了一個狀態(tài)而已盗胀。
我們經(jīng)常通過這樣的方式艘蹋,通過BuildContext,可以拿到ThemeMediaQuery

//得到狀態(tài)欄的高度
var statusBarHeight = MediaQuery.of(context).padding.top;
//復(fù)制合并出新的主題
var copyTheme =Theme.of(context).copyWith(primaryColor: Colors.blue);

看到of的靜態(tài)方法,第一反應(yīng)是去通過這個context去構(gòu)建新的類票灰。然后從這個類中女阀,去調(diào)用獲取狀態(tài)的方法。(Android開發(fā)的同學(xué)應(yīng)該很熟悉的套路屑迂,類似Picasso浸策、Glide)。但事實上是這樣嗎惹盼?

MediaQuery

通過context.inheritFromWidgetOfExactType
static MediaQueryData of(BuildContext context, { bool nullOk: false }) {
    assert(context != null);
    assert(nullOk != null);
    final MediaQuery query = context.inheritFromWidgetOfExactType(MediaQuery);
    if (query != null)
      return query.data;
    if (nullOk)
      return null;
    throw new FlutterError(
      'MediaQuery.of() called with a context that does not contain a MediaQuery.\n'
      'No MediaQuery ancestor could be found starting from the context that was passed '
      'to MediaQuery.of(). This can happen because you do not have a WidgetsApp or '
      'MaterialApp widget (those widgets introduce a MediaQuery), or it can happen '
      'if the context you use comes from a widget above those widgets.\n'
      'The context used was:\n'
      '  $context'
    );
  }
  • 首先庸汗,可以看到通過這個方法context.inheritFromWidgetOfExactType來查到MediaQuery
    MediaQuery是我們存在在BuildContext中的屬性手报。
  • 其次蚯舱,可以看到MediaQuery存儲在的BuildContext中的位置是在WidgetsApp.(因為其實MaterialApp返回的也是它)
MediaQuery狀態(tài)保存的原理
  • 繼承InheritedWidget

    image.png

  • 通過build方法中返回

  1. MaterialApp_MaterialAppState中的build方法

    image.png

  2. WidgetsApp_WidgetsAppState中的build方法

    image.png

  • 獲取
    最后就是最上面看到的那段代碼,通過context.inheritFromWidgetOfExactType來獲取掩蛤。
    然后在子樹的任何地方枉昏,都可以通過這樣的方式來進行獲取。

定義一個AppState

了解了MediaQuery的存放方式揍鸟,我們可以實現(xiàn)自己的狀態(tài)管理兄裂,這樣在子組件中,就可以同步獲取到狀態(tài)值阳藻。

0.先定義一個AppState
//0. 定義一個變量來存儲
class AppState {
  bool isLoading;

  AppState({this.isLoading = true});

  factory AppState.loading() => AppState(isLoading: true);

  @override
  String toString() {
    return 'AppState{isLoading: $isLoading}';
  }
}

1. 繼承InheritedWidget
//1. 模仿MediaQuery晰奖。簡單的讓這個持有我們想要保存的data
class _InheritedStateContainer extends InheritedWidget {
  final AppState data;

  //我們知道InheritedWidget總是包裹的一層,所以它必有child
  _InheritedStateContainer(
      {Key key, @required this.data, @required Widget child})
      : super(key: key, child: child);

  //參考MediaQuery,這個方法通常都是這樣實現(xiàn)的稚配。如果新的值和舊的值不相等畅涂,就需要notify
  @override
  bool updateShouldNotify(_InheritedStateContainer oldWidget) =>
      data != oldWidget.data;
}
2. 創(chuàng)建外層的Widget

創(chuàng)建外層的Widget,并且提供靜態(tài)方法of,來得到我們的AppState

/*
1. 從MediaQuery模仿的套路道川,我們知道午衰,我們需要一個StatefulWidget作為外層的組件,
將我們的繼承于InheritateWidget的組件build出去
*/
class AppStateContainer extends StatefulWidget {
  //這個state是我們需要的狀態(tài)
  final AppState state;

  //這個child的是必須的冒萄,來顯示我們正常的控件
  final Widget child;

  AppStateContainer({this.state, @required this.child});

  //4.模仿MediaQuery,提供一個of方法臊岸,來得到我們的State.
  static AppState of(BuildContext context) {
    //這個方法內(nèi),調(diào)用 context.inheritFromWidgetOfExactType
    return (context.inheritFromWidgetOfExactType(_InheritedStateContainer)
            as _InheritedStateContainer)
        .data;
  }

  @override
  _AppStateContainerState createState() => _AppStateContainerState();
}

class _AppStateContainerState extends State<AppStateContainer> {

  //2. 在build方法內(nèi)返回我們的InheritedWidget
  //這樣App的層級就是 AppStateContainer->_InheritedStateContainer-> real app
  @override
  Widget build(BuildContext context) {
    return _InheritedStateContainer(
      data: widget.state,
      child: widget.child,
    );
  }
}
3. 使用
  • 包括在最外層
class MyInheritedApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    //因為是AppState尊流,所以他的范圍是全生命周期的帅戒,所以可以直接包裹在最外層
    return AppStateContainer(
      //初始化一個loading
      state: AppState.loading(),
      child: new MaterialApp(
        title: 'Flutter Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: new MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}
  • 在任何你想要的位置中,使用。
    文檔里面推薦逻住,在didChangeDependencies中查詢它钟哥。所以我們也
class _MyHomePageState extends State<MyHomePage> {
  _MyHomePageState() {}

   AppState appState;
  //在didChangeDependencies方法中,就可以查到對應(yīng)的state了
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('didChangeDependencies');
    if(appState==null){
      appState= AppStateContainer.of(context);
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text(widget.title),
        ),
        body: new Center(
          //根據(jù)isLoading來判斷瞎访,顯示一個loading腻贰,或者是正常的圖
          child: appState.isLoading
              ? CircularProgressIndicator()
              : new Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new Text(
                      'appState.isLoading = ${appState.isLoading}',
                    ),
                  ],
                ),
        ),
        floatingActionButton: new Builder(builder: (context) {
          return new FloatingActionButton(
            onPressed: () {
              //點擊按鈕進行切換
              //因為是全局的狀態(tài),在其他頁面改變扒秸,也會導(dǎo)致這里發(fā)生變化
              appState.isLoading = !appState.isLoading;
              //setState觸發(fā)頁面刷新
              setState(() {});
            },
            tooltip: 'Increment',
            child: new Icon(Icons.swap_horiz),
          );
        }));
  }
}
運行效果1-當(dāng)前頁面

點擊按鈕更改狀態(tài)播演。


21.gif
4. 在另外一個頁面修改AppState

因為上面代碼是在一個頁面內(nèi)的情況,我們要確定是否全局的狀態(tài)是保持一致的伴奥。所以
讓我們再改一下代碼写烤,點擊push出新的頁面,在新頁面內(nèi)改變appState的狀態(tài)拾徙,看看就頁面會不會發(fā)生變化洲炊。
代碼修改如下:

//修改floatingButton的點擊事件
  floatingActionButton: new Builder(builder: (context) {
          return new FloatingActionButton(
            onPressed: () {
              //push出一個先的頁面              
              Navigator.of(context).push(
                  new MaterialPageRoute<Null>(builder: (BuildContext context) {
                return MyHomePage(
                    title: 'Second State Change Page');
              }));
            //注釋掉原來的代碼
//              appState.isLoading = !appState.isLoading;
//              setState(() {});
            },
            tooltip: 'Increment',
            child: new Icon(Icons.swap_horiz),
          );
        })

  • 新增的MyHomePage
    基本上和上面的代碼一致。同樣讓他修改appState
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  void _changeState() {
    setState(() {
      state.isLoading = !state.isLoading;
    });
  }

  AppState state;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if(state ==null){
      state = AppStateContainer.of(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'appState.isLoading = ${state.isLoading}',
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _changeState,
        tooltip: 'ChangeState',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
運行效果2-另外一個頁面內(nèi)修改狀態(tài)

在push的頁面修改AppState的狀態(tài)尼啡,回到初始的頁面选浑,看狀態(tài)是否發(fā)生變化。


21.gif

小結(jié)和思考

通過分析MediaQuery玄叠,我們了解到了InheritedWidget的用法,并且通過自定義的AppState等操作熟悉了整體狀態(tài)控制的流程拓提。
我們可以繼續(xù)思考下面幾個問題

  • 為什么AppState能在整個App周期中读恃,維持狀態(tài)呢?
    因為我們將其包裹在了最外層代态。
    由此思考寺惫,每個頁面可能也有自己的狀態(tài),維護頁面的狀態(tài)蹦疑,可以將其包裹在頁面的層級的最外層西雀,這樣它就變成了PageScope的狀態(tài)了。

  • 限制-like a EventBus
    當(dāng)我們改變state并關(guān)閉頁面后歉摧,因為didChangeDependencies方法和build方法的執(zhí)行艇肴,我們打開這個頁面時,總能拿到最新的state叁温。所以我們的頁面能夠同步狀態(tài)成功再悼。
    那如果是像EventBus一樣,push出一個狀態(tài)膝但,我們需要去進行一個耗時操作冲九,然后才能發(fā)生的改變我們能監(jiān)聽和處理嗎?

ValueNotifier

繼承至ChangeNotifier跟束≥杭椋可以注冊監(jiān)聽事件丑孩。當(dāng)值發(fā)生改變時,會給監(jiān)聽則發(fā)送監(jiān)聽灭贷。

/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced, this class notifies its listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  /// Creates a [ChangeNotifier] that wraps this value.
  ValueNotifier(this._value);

  /// The current value stored in this notifier.
  ///
  /// When the value is replaced, this class notifies its listeners.
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

源碼看到温学,只要改變值value值,相當(dāng)于調(diào)用set方法氧腰,都會notifyListeners

修改代碼

AppState添加成員
//定義一個變量來存儲
class AppState {
 //...忽略重復(fù)代碼枫浙。添加成員變量
  ValueNotifier<bool> canListenLoading = ValueNotifier(false);
}
_MyHomeInheritedPageState 中添加監(jiān)聽
class _MyHomeInheritedPageState extends State<MyInheritedHomePage> {
 //...忽略重復(fù)代碼。添加成員變量

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('didChangeDependencies');
    if (appState == null) {
      print('state == null');
      appState = AppStateContainer.of(context);
      //在這里添加監(jiān)聽事件
      appState.canListenLoading.addListener(listener);
    }
  }

  @override
  void dispose() {
    print('dispose');
    if (appState != null) {
      //在這里移除監(jiān)聽事件
      appState.canListenLoading.removeListener(listener);
    }
    super.dispose();
  }

  @override
  void initState() {
    print('initState');
    //初始化監(jiān)聽的回調(diào)古拴÷嶂悖回調(diào)用作的就是延遲5s后,將result修改成 "From delay"
    listener = () {
      Future.delayed(Duration(seconds: 5)).then((value) {
        result = "From delay";
        setState(() {});
      });
    };
    super.initState();
  }

  //添加成員變量黄痪。 result參數(shù)和 listener回調(diào)
  String result = "";
  VoidCallback listener;

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text(widget.title),
        ),
        body: new Center(
          child: appState.isLoading
              ? CircularProgressIndicator()
              : new Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new Text(
                      'appState.isLoading = ${appState.isLoading}',
                    ),
                    //新增紧帕,result的顯示在屏幕上
                    new Text(
                      '${result}',
                    ),
                  ],
                ),
        ),
       //...忽略重復(fù)代碼
  }
}
運行結(jié)果

運行結(jié)果和我們預(yù)想的一樣。

  • 顯示打開一個新的頁面桅打。
  • 在新的頁面內(nèi)改變canListenLoading的value是嗜。這樣會觸發(fā)上一個頁面已經(jīng)注冊的監(jiān)聽事件(4s后改變值)。
  • 然后我們退回來挺尾,等待后確實發(fā)現(xiàn)了數(shù)據(jù)發(fā)生了變化~~


    21.gif

這樣就感覺可以實現(xiàn)一個類似EventBus的功能了~~

總結(jié)

這邊文章鹅搪,主要說的是,利用Flutter自身的框架來實現(xiàn)遭铺,狀態(tài)管理和消息傳遞的內(nèi)容丽柿。

  • 通過InheritedWidget來保存狀態(tài)
  • 通過context.inheritFromWidgetOfExactType來獲取屬性
  • 使用ValueNotifer來實現(xiàn)屬性監(jiān)聽。

我們可以對狀態(tài)管理做一個小結(jié)

  • Key
    保存Widget的狀態(tài)魂挂,我們可以通過給對應(yīng)Widgetkey,來保存狀態(tài)甫题,并通過Key來拿到狀態(tài)。
    比如是 ObjectKey可以在列表中標記唯一的Key涂召,來保存狀態(tài)坠非,讓動畫識別。
    GlobalKey果正,則可以保存一個狀態(tài)炎码,其他地方都可以獲取。

  • InheritedWidget
    可以持有一個狀態(tài)舱卡,共它的子樹來獲取辅肾。
    這樣子樹本身可以不直接傳入這個字段(這樣可以避免多級的Widget時,要一層一層向下傳遞狀態(tài))
    還可以做不同Widget中間的狀態(tài)同步

  • ChangeNofier
    繼承這里類轮锥,我們就可以實現(xiàn)Flutter中的觀察者模式矫钓,對屬性變化做觀察。

另外,我們還可以通過第三方庫新娜,比如說 ReduxScopeModel Rx來做這個事情赵辕。但是其基于的原理,應(yīng)該也是上方的內(nèi)容概龄。


從下往上傳遞分發(fā)數(shù)據(jù)还惠、狀態(tài)

Notification

我們知道,我們可以通過NotificationListener的方式來監(jiān)聽ScrollNotification頁面的滾動情況私杜。Flutter中就是通過這樣的方式蚕键,通過來從子組件往父組件的BuildContext中發(fā)布數(shù)據(jù),完成數(shù)據(jù)傳遞的衰粹。
下面我們簡單的來實現(xiàn)一個我們自己的锣光。

  • 代碼
//0.自定義一個Notification。
class MyNotification extends Notification {}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    //2.在Scaffold的層級進行事件的監(jiān)聽铝耻。創(chuàng)建`NotificationListener`,并在`onNotification`就可以得到我們的事件了誊爹。
    return NotificationListener(
        onNotification: (event) {
          if (event is MyNotification) {
            print("event= Scaffold MyNotification");
          }
        },
        child: new Scaffold(
            appBar: new AppBar(
              title: new Text(widget.title),
            ),
          //3.注意,這里是監(jiān)聽不到事件的瓢捉。這里需要監(jiān)聽到事件频丘,需要在body自己的`BuildContext`發(fā)送事件才行!E萏B!
            body: new NotificationListener<MyNotification>(
                onNotification: (event) {
                  //接受不到事件某弦,因為`context`不同
                  print("body event=" + event.toString());
                },
                child: new Center(
                  child: new Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      new Text(
                        'appState.isLoading = ',
                      ),
                      new Text(
                        'appState.canListenLoading.value',
                      ),
                    ],
                  ),
                )),
            floatingActionButton: Builder(builder: (context) {
              return FloatingActionButton(
                onPressed: () {
                  //1.創(chuàng)建事件状答,并通過發(fā)送到對應(yīng)的`BuildContext`中。注意刀崖,這里的`context`是`Scaffold`的`BuildContext`
                  new MyNotification().dispatch(context);
                },
                tooltip: 'ChangeState',
                child: new Icon(Icons.add),
              );
            })));
  }
}

  • 運行結(jié)果


    image.png

小結(jié)

我們可以通過Notification的繼承類,將其發(fā)布到對應(yīng)的buildContext中拍摇,來實現(xiàn)數(shù)據(jù)傳遞亮钦。


總結(jié)

通過這邊Flutter數(shù)據(jù)傳遞的介紹,我們可以大概搭建自己的Flutter App的數(shù)據(jù)流結(jié)構(gòu)充活。
類似閑魚的界面的架構(gòu)設(shè)計蜂莉。

閑魚flutter的界面框架設(shè)計.png
  • 從上往下:
    通過自定義不同ScopeInheritedWidget來hold住不同Scope的數(shù)據(jù),這樣當(dāng)前Scope下的子組件都能得到對應(yīng)的數(shù)據(jù)混卵,和得到對應(yīng)的更新映穗。

  • 從下往上:
    通過自定義的Notification類。在子組件中通過Notification(data).dispatch(context)這樣的方式發(fā)布幕随,在對應(yīng)的Context上蚁滋,在通過NotificationListener進行捕獲和監(jiān)聽。

最后

通過三遍文章,對Flutter文檔中一些細節(jié)做了必要的入門補充辕录。
還沒有介紹相關(guān)的 手勢睦霎,網(wǎng)絡(luò)請求Channel和Native通信走诞,還有動畫等內(nèi)容副女。請結(jié)合文檔學(xué)習(xí)。

在豐富了理論知識之后蚣旱,下一編開始碑幅,我們將進行Flutter的實戰(zhàn)分析。

參考文章

Build reactive mobile apps in Flutter?—?companion article
set-up-inherited-widget-app-state
深入了解Flutter界面開發(fā)(強烈推薦) (ps:真的強烈推薦)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末塞绿,一起剝皮案震驚了整個濱河市沟涨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌位隶,老刑警劉巖拷窜,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異涧黄,居然都是意外死亡篮昧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進店門笋妥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來懊昨,“玉大人,你說我怎么就攤上這事春宣〗桶洌” “怎么了?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵月帝,是天一觀的道長躏惋。 經(jīng)常有香客問我,道長嚷辅,這世上最難降的妖魔是什么簿姨? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮簸搞,結(jié)果婚禮上扁位,老公的妹妹穿的比我還像新娘。我一直安慰自己趁俊,他們只是感情好域仇,可當(dāng)我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寺擂,像睡著了一般暇务。 火紅的嫁衣襯著肌膚如雪泼掠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天般卑,我揣著相機與錄音武鲁,去河邊找鬼。 笑死蝠检,一個胖子當(dāng)著我的面吹牛沐鼠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叹谁,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼饲梭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了焰檩?” 一聲冷哼從身側(cè)響起憔涉,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎析苫,沒想到半個月后兜叨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡衩侥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年国旷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茫死。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡跪但,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出峦萎,到底是詐尸還是另有隱情屡久,我是刑警寧澤,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布爱榔,位于F島的核電站被环,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏详幽。R本人自食惡果不足惜蛤售,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望妒潭。 院中可真熱鬧,春花似錦揣钦、人聲如沸雳灾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谎亩。三九已至炒嘲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間匈庭,已是汗流浹背夫凸。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阱持,地道東北人夭拌。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像衷咽,于是被迫代替她去往敵國和親鸽扁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,781評論 2 361

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