Flutter完整開發(fā)實(shí)戰(zhàn)詳解(十二、全面深入理解狀態(tài)管理設(shè)計(jì))

作為系列文章的第十二篇,本篇將通過 scope_model 、 BloC 設(shè)計(jì)模式、flutter_redux 、 fish_redux 來全面深入分析, Flutter 中大家最為關(guān)心的狀態(tài)管理機(jī)制,理解各大框架中如何設(shè)計(jì)實(shí)現(xiàn)狀態(tài)管理,從而選出你最為合適的 state “大管家”。

文章匯總地址:

Flutter 完整實(shí)戰(zhàn)實(shí)戰(zhàn)系列文章專欄

Flutter 番外的世界系列文章專欄

在所有 響應(yīng)式編程 中,狀態(tài)管理一直老生常談的話題,而在 Flutter 中,目前主流的有 scope_model 、BloC 設(shè)計(jì)模式flutter_redux 、fish_redux 等四種設(shè)計(jì),它們的 復(fù)雜度上手難度 是逐步遞增的,但同時(shí) 可拓展性 、解耦度復(fù)用能力 也逐步提升。

基于前篇,我們對 Stream 已經(jīng)有了全面深入的理解,后面可以發(fā)現(xiàn)這四大框架或多或少都有 Stream 的應(yīng)用,不過還是那句老話,合適才是最重要,不要為了設(shè)計(jì)而設(shè)計(jì)

本文Demo源碼

GSYGithubFlutter 完整開源項(xiàng)目

一、scoped_model

scoped_model 是 Flutter 最為簡單的狀態(tài)管理框架,它充分利用了 Flutter 中的一些特性,只有一個(gè) dart 文件的它,極簡的實(shí)現(xiàn)了一般場景下的狀態(tài)管理。

如下方代碼所示,利用 scoped_model 實(shí)現(xiàn)狀態(tài)管理只需要三步 :

  • 定義 Model 的實(shí)現(xiàn),如 CountModel ,并且在狀態(tài)改變時(shí)執(zhí)行 notifyListeners() 方法。
  • 使用 ScopedModel Widget 加載 Model 。
  • 使用 ScopedModelDescendant 或者 ScopedModel.of<CountModel>(context) 加載 Model 內(nèi)狀態(tài)數(shù)據(jù)。

是不是很簡單?那僅僅一個(gè) dart 文件,如何實(shí)現(xiàn)這樣的效果的呢?后面我們馬上開始剝析它。

class ScopedPage extends StatelessWidget {
  final CountModel _model = new CountModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("scoped"),
        ),
        body: Container(
          child: new ScopedModel<CountModel>(
            model: _model,
            child: CountWidget(),
          ),
        ));
  }
}

class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
        return new Column(
          children: <Widget>[
            new Expanded(child: new Center(child: new Text(model.count.toString()))),
            new Center(
              child: new FlatButton(
                  onPressed: () {
                    model.add();
                  },
                  color: Colors.blue,
                  child: new Text("+")),
            ),
          ],
        );
      });
  }
}

class CountModel extends Model {
  static CountModel of(BuildContext context) =>
      ScopedModel.of<CountModel>(context);
  
  int _count = 0;
  
  int get count => _count;
  
  void add() {
    _count++;
    notifyListeners();
  }
}

如下圖所示,在 scoped_model 的整個(gè)實(shí)現(xiàn)流程中,ScopedModel 這個(gè) Widget 很巧妙的借助了 AnimatedBuildler 。

因?yàn)? AnimatedBuildler 繼承了 AnimatedWidget ,在 AnimatedWidget 的生命周期中會(huì)對 Listenable 接口添加監(jiān)聽,而 Model 恰好就實(shí)現(xiàn)了 Listenable 接口,整個(gè)流程總結(jié)起來就是:

  • Model 實(shí)現(xiàn)了 Listenable 接口,內(nèi)部維護(hù)一個(gè) Set<VoidCallback> _listeners 。
  • 當(dāng) Model 設(shè)置給 AnimatedBuildler 時(shí), ListenableaddListener 會(huì)被調(diào)用,然后添加一個(gè) _handleChange 監(jiān)聽到 _listeners 這個(gè) Set 中。
  • 當(dāng) Model 調(diào)用 notifyListeners 時(shí),會(huì)通過異步方法 scheduleMicrotask 去從頭到尾執(zhí)行一遍 _listeners 中的 _handleChange。
  • _handleChange 監(jiān)聽被調(diào)用,執(zhí)行了 setState({}) 。
image.png

整個(gè)流程是不是很巧妙,機(jī)制的利用了 AnimatedWidgetListenable 在 Flutter 中的特性組合,至于 ScopedModelDescendant 就只是為了跨 Widget 共享 Model 而做的一層封裝,主要還是通過 ScopedModel.of<CountModel>(context) 獲取到對應(yīng) Model 對象,這這個(gè)實(shí)現(xiàn)上,scoped_model 依舊利用了 Flutter 的特性控件 InheritedWidget 實(shí)現(xiàn)。

InheritedWidget

scoped_model 中我們可以通過 ScopedModel.of<CountModel>(context) 獲取我們的 Model ,其中最主要是因?yàn)槠鋬?nèi)部的 build 的時(shí)候,包裹了一個(gè) _InheritedModel 控件,而它繼承了 InheritedWidget 。

為什么我們可以通過 context 去獲取到共享的 Model 對象呢?

首先我們知道 context 只是接口,而在 Flutter 中 context 的實(shí)現(xiàn)是 Element ,在 ElementinheritFromWidgetOfExactType 方法實(shí)現(xiàn)里,有一個(gè) Map<Type, InheritedElement> _inheritedWidgets 的對象。

_inheritedWidgets 一般情況下是空的,只有當(dāng)父控件是 InheritedWidget 或者本身是 InheritedWidgets 時(shí)才會(huì)有被初始化,而當(dāng)父控件是 InheritedWidget 時(shí),這個(gè) Map 會(huì)被一級一級往下傳遞與合并 。

所以當(dāng)我們通過 context 調(diào)用 inheritFromWidgetOfExactType 時(shí),就可以往上查找到父控件的 Widget,從在 scoped_model 獲取到 _InheritedModel 中的Model 。

二、BloC

BloC 全稱 Business Logic Component ,它屬于一種設(shè)計(jì)模式,在 Flutter 中它主要是通過 StreamSteamBuilder 來實(shí)現(xiàn)設(shè)計(jì)的,所以 BloC 實(shí)現(xiàn)起來也相對簡單,關(guān)于 StreamSteamBuilder 的實(shí)現(xiàn)原理可以查看前篇,這里主要展示如何完成一個(gè)簡單的 BloC 。

如下代碼所示,整個(gè)流程總結(jié)為:

  • 定義一個(gè) PageBloc 對象,利用 StreamController 創(chuàng)建 SinkStream
  • PageBloc 對外暴露 Stream 用來與 StreamBuilder 結(jié)合;暴露 add 方法提供外部調(diào)用,內(nèi)部通過 Sink 更新 Stream
  • 利用 StreamBuilder 加載監(jiān)聽 Stream 數(shù)據(jù)流,通過 snapShot 中的 data 更新控件。

當(dāng)然,如果和 rxdart 結(jié)合可以簡化 StreamController 的一些操作,同時(shí)如果你需要利用 BloC 模式實(shí)現(xiàn)狀態(tài)共享,那么自己也可以封裝多一層 InheritedWidgets 的嵌套,如果對于這一塊有疑惑的話,推薦可以去看看上一篇的 Stream 解析。

class _BlocPageState extends State<BlocPage> {
  final PageBloc _pageBloc = new PageBloc();
  @override
  void dispose() {
    _pageBloc.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: new StreamBuilder(
            initialData: 0,
            stream: _pageBloc.stream,
            builder: (context, snapShot) {
              return new Column(
                children: <Widget>[
                  new Expanded(
                      child: new Center(
                          child: new Text(snapShot.data.toString()))),
                  new Center(
                    child: new FlatButton(
                        onPressed: () {
                          _pageBloc.add();
                        },
                        color: Colors.blue,
                        child: new Text("+")),
                  ),
                  new SizedBox(
                    height: 100,
                  )
                ],
              );
            }),
      ),
    );
  }
}
class PageBloc {
  int _count = 0;
  ///StreamController
  StreamController<int> _countController = StreamController<int>();
  ///對外提供入口
  StreamSink<int> get _countSink => _countController.sink;
  ///提供 stream StreamBuilder 訂閱
  Stream<int> get stream => _countController.stream;
  void dispose() {
    _countController.close();
  }
  void add() {
    _count++;
    _countSink.add(_count);
  }
}

三、flutter_redux

相信如果是前端開發(fā)者,對于 redux 模式并不會(huì)陌生,而 flutter_redux 可以看做是利用了 Stream 特性的 scope_model 升級版,通過 redux 設(shè)計(jì)模式來完成解耦和拓展。

當(dāng)然,更多的功能和更好的拓展性,也造成了代碼的復(fù)雜度和上手難度 ,因?yàn)? flutter_redux 的代碼使用篇幅問題,這里就不展示所有代碼了,需要看使用代碼的可直接從 demo 獲取,現(xiàn)在我們直接看 flutter_redux 是如何實(shí)現(xiàn)狀態(tài)管理的吧。

image

如上圖,我們知道 redux 中一般有 Store 、 Action 、 Reducer 三個(gè)主要對象,之外還有 Middleware 中間件用于攔截,所以如下代碼所示:

  • 創(chuàng)建 Store 用于管理狀態(tài) 。
  • Store 增加 appReducer 合集方法,增加需要攔截的 middleware,并初始化狀態(tài)。
  • Store 設(shè)置給 StoreProvider 這個(gè) InheritedWidget 。
  • 通過 StoreConnector / StoreBuilder 加載顯示 Store 中的數(shù)據(jù)。

之后我們可以 dispatch 一個(gè) Action ,在經(jīng)過 middleware 之后,觸發(fā)對應(yīng)的 Reducer 返回?cái)?shù)據(jù),而事實(shí)上這里核心的內(nèi)容實(shí)現(xiàn),還是 StreamStreamBuilder 的結(jié)合使用 ,接下來就讓我們看看這個(gè)流程是如何聯(lián)動(dòng)起來的吧。

class _ReduxPageState extends State<ReduxPage> {

  ///初始化store
  final store = new Store<CountState>(
    /// reducer 合集方法
    appReducer,
    ///中間鍵
    middleware: middleware,
    ///初始化狀態(tài)
    initialState: new CountState(count: 0),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("redux"),
        ),
        body: Container(
          /// StoreProvider InheritedWidget
          /// 加載 store 共享
          child: new StoreProvider(
            store: store,
            child: CountWidget(),
          ),
        ));
  }
}

如下圖所示,是 flutter_redux 從入口到更新的完整流程圖,整理這個(gè)流程其中最關(guān)鍵有幾個(gè)點(diǎn)是:

  • StoreProviderInheritedWidgets ,所以它可以通過 context 實(shí)現(xiàn)狀態(tài)共享。
  • StreamBuilder / StoreConnector 的內(nèi)部實(shí)現(xiàn)主要是 StreamBuilder
  • Store 內(nèi)部是通過 StreamController.broadcast 創(chuàng)建的 Stream ,然后在 StoreConnector 中通過 Streammap 、transform 實(shí)現(xiàn)小狀態(tài)的變換,最后更新到 StreamBuilder 。

那么現(xiàn)在看下圖流程有點(diǎn)暈?下面我們直接分析圖中流程。

image

可以看出整個(gè)流程的核心還是 Stream ,基于這幾個(gè)關(guān)鍵點(diǎn),我們把上圖的流程整理為:

  • 1、 Store 創(chuàng)建時(shí)傳入 reducer 對象和 middleware 數(shù)組,同時(shí)通過 StreamController.broadcast 創(chuàng)建了 _changeController 對象。
  • 2、 Store 利用 middleware_changeController 組成了一個(gè) NextDispatcher 方法數(shù)組 ,并把 _changeController 所在的 NextDispatcher 方法放置在數(shù)組最后位置。
  • 3、 StoreConnector 內(nèi)通過 Store_changeController 獲取 Stream ,并進(jìn)行了一系列變換后,最終 Stream 設(shè)置給了 StreamBuilder。
  • 4、當(dāng)我們調(diào)用 Stroedispatch 方法時(shí),我們會(huì)先進(jìn)過 NextDispatcher 數(shù)組中的一系列 middleware 攔截器,最終調(diào)用到隊(duì)末的 _changeController 所在的 NextDispatcher。
  • 5、最后一個(gè) NextDispatcher 執(zhí)行時(shí)會(huì)先執(zhí)行 reducer 方法獲取新的 state ,然后通過 _changeController.add 將狀態(tài)加載到 Stream 流程中,觸發(fā) StoreConnectorStreamBuilder 更新數(shù)據(jù)。

如果對于 Stream 流程不熟悉的還請看上篇。

現(xiàn)在再對照流程圖會(huì)不會(huì)清晰很多了?

flutter_redux 中,開發(fā)者的每個(gè)操作都只是一個(gè) Action ,而這個(gè)行為所觸發(fā)的邏輯完全由 middlewarereducer 決定,這樣的設(shè)計(jì)在一定程度上將業(yè)務(wù)與UI隔離,同時(shí)也統(tǒng)一了狀態(tài)的管理。

比如你一個(gè)點(diǎn)擊行為只是發(fā)出一個(gè) RefrshAction ,但是通過 middleware 攔截之后,在其中異步處理完幾個(gè)數(shù)據(jù)接口,然后重新 dispatchAction1、Action2 、Action3 去更新其他頁面, 類似的 redux_epics 庫就是這樣實(shí)現(xiàn)異步的 middleware 邏輯。

四、fish_redux

如果說 flutter_redux 屬于相對復(fù)雜的狀態(tài)管理設(shè)置的話,那么閑魚開源的 fish_redux 可謂 “不走尋常路” 了,雖然是基于 redux 原有的設(shè)計(jì)理念,同時(shí)也有使用到 Stream ,但是相比較起來整個(gè)設(shè)計(jì)完全是 超脫三界,如果是前面的都是簡單的拼積木,那是 fish_redux 就是積木界的樂高。

image

因?yàn)槠颍@里也只展示部分代碼,其中 reducer 還是我們熟悉的存在,而閑魚在這 redux 的基礎(chǔ)上提出了 Comoponent 的概念,這個(gè)概念下 fish_redux 是從 Context 、Widget 等地方就開始全面“入侵”你的代碼,從而帶來“超級賽亞人”版的 redux 。

如下代碼所示,默認(rèn)情況我們需要:

  • 繼承 Page 實(shí)現(xiàn)我們的頁面。
  • 定義好我們的 State 狀態(tài)。
  • 定義 effect 、 middlewarereducer 用于實(shí)現(xiàn)副作用、中間件、結(jié)果返回處理。
  • 定義 view 用于繪制頁面。
  • 定義 dependencies 用戶裝配控件,這里最騷氣的莫過于重載了 + 操作符,然后利用 ConnectorState 挑選出數(shù)據(jù),然后通過 Component 繪制。

現(xiàn)在看起來使用流程是不是變得復(fù)雜了?

但是這帶來的好處就是 復(fù)用的顆粒度更細(xì)了,裝配和功能更加的清晰。 那這個(gè)過程是如何實(shí)現(xiàn)的呢?后面我們將分析這個(gè)復(fù)雜的流程。

class FishPage extends Page<CountState, Map<String, dynamic>> {
  FishPage()
      : super(
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          ///配置 View 顯示
          view: buildView,
          ///配置 Dependencies 顯示
          dependencies: Dependencies<CountState>(
              slots: <String, Dependent<CountState>>{
                ///通過 Connector() 從 大 state 轉(zhuǎn)化處小 state
                ///然后將數(shù)據(jù)渲染到 Component
                'count-double': DoubleCountConnector() + DoubleCountComponent()
              }
          ),
          middleware: <Middleware<CountState>>[
            ///中間鍵打印log
            logMiddleware(tag: 'FishPage'),
          ]
  );
}

///渲染主頁
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
      appBar: AppBar(
        title: new Text("fish"),
      ),
      body: new Column(
        children: <Widget>[
          ///viewService 渲染 dependencies
          viewService.buildComponent('count-double'),
          new Expanded(child: new Center(child: new Text(state.count.toString()))),
          new Center(
            child: new FlatButton(
                onPressed: () {
                  ///+
                  dispatch(CountActionCreator.onAddAction());
                },
                color: Colors.blue,
                child: new Text("+")),
          ),
          new SizedBox(
            height: 100,
          )
        ],
      ));
}

如下大圖所示,整個(gè)聯(lián)動(dòng)的流程比 flutter_redux 復(fù)雜了更多( 如果看不清可以點(diǎn)擊大圖 ),而這個(gè)過程我們總結(jié)起來就是:

  • 1、Page 的構(gòu)建需要 State 、Effect 、Reducerview 、dependenciesmiddleware 等參數(shù)。

  • 2、Page 的內(nèi)部 PageProvider 是一個(gè) InheritedWidget 用戶狀態(tài)共享。

  • 3、Page 內(nèi)部會(huì)通過 createMixedStore 創(chuàng)建 Store 對象。

  • 4、Store 對象對外提供的 subscribe 方法,在訂閱時(shí)會(huì)將訂閱的方法添加到內(nèi)部 List<_VoidCallback> _listeners 。

  • 5、Store 對象內(nèi)部的 StreamController.broadcast 創(chuàng)建出了 _notifyController 對象用于廣播更新。

  • 6、Store 對象內(nèi)部的 subscribe 方法,會(huì)在 ComponentState 中添加訂閱方法 onNotify,如果調(diào)用在 onNotify 中最終會(huì)執(zhí)行 setState更新UI。

  • 7、Store 對象對外提供的 dispatch 方法,執(zhí)行時(shí)內(nèi)部會(huì)執(zhí)行 4 中的 List<_VoidCallback> _listeners,觸發(fā) onNotify。

  • 8、Page 內(nèi)部會(huì)通過 Logic 創(chuàng)建 Dispatch ,執(zhí)行時(shí)經(jīng)歷 Effect -> Middleware -> Stroe.dispatch -> Reducer -> State -> _notifyController -> _notifyController.add(state) 等流程。

  • 9、以上流程最終就是 Dispatch 觸發(fā) Store 內(nèi)部 _notifyController , 最終會(huì)觸發(fā) ComponentState 中的 onNotify 中的setState更新UI

image

是不是有很多對象很陌生?

確實(shí) fish_redux 的整體流程更加復(fù)雜,內(nèi)部的 ContxtSys 、ComponetViewSerivceLogic 等等概念設(shè)計(jì),這里因?yàn)槠邢蘧筒辉敿?xì)拆分展示了,但從整個(gè)流程可以看出 fish_redux控件到頁面更新,全都進(jìn)行了新的獨(dú)立設(shè)計(jì),而這里面最有意思的,莫不過 dependencies 。

如下圖所示,得益于fish_redux 內(nèi)部 ConnOpMixin 中對操作符的重載,我們可以通過 DoubleCountConnector() + DoubleCountComponent() 來實(shí)現(xiàn)Dependent 的組裝。

image

Dependent 的組裝中 Connector 會(huì)從總 State 中讀取需要的小 State 用于 Component 的繪制,這樣很好的達(dá)到了 模塊解耦與復(fù)用 的效果。

而使用中我們組裝的 dependencies 最后都會(huì)通過 ViewService 提供調(diào)用調(diào)用能力,比如調(diào)用 buildAdapter 用于列表能力,調(diào)用 buildComponent 提供獨(dú)立控件能力等。

可以看出 flutter_redux 的內(nèi)部實(shí)現(xiàn)復(fù)雜度是比較高的,在提供組裝、復(fù)用、解耦的同時(shí),也對項(xiàng)目進(jìn)行了一定程度的入侵,這里的篇幅可能不能很全面的分析 flutter_redux 中的整個(gè)流程,但是也能讓你理解整個(gè)流程的關(guān)鍵點(diǎn),細(xì)細(xì)品味設(shè)計(jì)之美。

自此,第十二篇終于結(jié)束了!(///▽///)

資源推薦

完整開源項(xiàng)目推薦:
我們還會(huì)再見嗎?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評論 2 374

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