FlutterBloc實(shí)戰(zhàn)應(yīng)用

Flutter Bloc

英文全稱(business logic),用于處理業(yè)務(wù)邏輯,其內(nèi)部實(shí)現(xiàn)主要是對(duì)Stream的輸入和輸出進(jìn)行了封裝,它的實(shí)現(xiàn)原理是利用RxDart(基于Stream封裝)提供的PublishSubjectBehivorSubject實(shí)現(xiàn)了EventState之間的轉(zhuǎn)換,以及利用Flutter提供的局部狀態(tài)管理控件InheritedWidget來傳遞Bloc對(duì)象,涉及的類比較多,但基本都圍繞著對(duì)上面這個(gè)對(duì)象的封裝。

Usage

https://bloclibrary.dev/

WorkFlow

  • Provider負(fù)責(zé)提供Bloc對(duì)象,注冊(cè)到當(dāng)前的context中
  • Bloc接口來自service和ui的事件,并將其轉(zhuǎn)換為對(duì)應(yīng)的state
  • service和UI根據(jù)接受的state進(jìn)行更新各自的狀態(tài)
  • 利用hydrateBloc對(duì)bloc的信息進(jìn)行緩存
截屏2020-09-30 上午6.26.53.png

主要類之間的關(guān)系

FlutterBloc Classes.png

實(shí)現(xiàn)步驟

  1. 首先自定義Bloc

    自定義一個(gè)繼承Bloc的類,并實(shí)現(xiàn)mapEventToState的方法邏輯更胖,完成事件輸入到數(shù)據(jù)輸出之間的轉(zhuǎn)換,

  2. 注冊(cè)Bloc

BlocProvider(
    create: (context) => MyCustomBloc(),
    child: YourCustomWidget(),
);
  • 這里需要用到BlocProvider來包裝Bloc的構(gòu)造方法和它的獲取方法
    BlocProvider繼承于ValueDelegateWidget,并實(shí)現(xiàn)了SingleChildCloneableWidget,包含一個(gè)ValueStateDelegate對(duì)象

  • ValueStateDelegate主要是用于包裝Bloc對(duì)象,用于控制是否更新或者銷毀bloc

  • ValueDelegateWidget繼承于DelegateWidget,在其State事件initState,didUpdateWidget,dispose中對(duì)ValueStateDelegate進(jìn)行去重,更新和銷毀操作,這樣就能間接作用于bloc上(這樣減少了Bloc的方法,起到了一定的解耦合作用),StateDelegate分為2中,BuilderStateDelegate支持關(guān)閉bloc,而ValueStateDelegate不會(huì)自動(dòng)關(guān)閉bloc,在使用中如果不是全局單例Bloc,則容易出現(xiàn)內(nèi)存泄漏

-BlocProvider在build時(shí)他會(huì)在內(nèi)部創(chuàng)建一個(gè)InheritedProvider,它接受了一個(gè)Bloc作為值,由于InheritedProvider是繼承于InheritedWidget,所以Bloc就可以通過InhertedElement間接的存儲(chǔ)到當(dāng)前的的Element中,它的child內(nèi)部就能直接通過Context獲取到.

  • SingleChildCloneableWidget這個(gè)類主要是定義了一個(gè)clone的協(xié)議徒蟆,主要是為后面的MutliBlocProvider服務(wù)的,將單個(gè)child拷貝多分,設(shè)置到每個(gè)BlocProvider中,是框架內(nèi)部的一個(gè)語法糖拯啦。

  • BlocProvider注冊(cè)的過程中,可以看到它必須要又一個(gè)child,它自己本身是不需要在界面顯示,只是作為一個(gè)數(shù)據(jù)的提供者,所有在它的之下的widget都能通過BlocProvder.of<BlocName>(context)獲取到這個(gè)對(duì)應(yīng)的Bloc郎逃。

class BlocProvider<T extends Bloc<dynamic, dynamic>>
    extends ValueDelegateWidget<T> implements SingleChildCloneableWidget { 
  final Widget child;

  //BlocProvider銷毀時(shí)bloc會(huì)被自動(dòng)關(guān)閉
  BlocProvider({ ...
    @required ValueBuilder<T> create,//在init時(shí)執(zhí)行此方法創(chuàng)建bloc
    Widget child,
  })  

  //BlocProvider銷毀時(shí)bloc不會(huì)自動(dòng)關(guān)閉
  BlocProvider.value({ ...
    @required T value, //bloc
    Widget child, //具體的頁面
  });

  //這里只是用`Provider`的類方法包裝了一層,將通過InheritedElment獲取方法`context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>`
  static T of<T extends Bloc<dynamic, dynamic>>(BuildContext context) { ...
    
  //通過`InheritedProvider`存儲(chǔ)bloc,提供provider獲取的方法
  @override
  Widget build(BuildContext context) {
    return InheritedProvider<T>(
      value: delegate.value,
      child: child,
    );
  }
  
  //方便MutliBlocProvider使用
  @override
  BlocProvider<T> cloneWithChild(Widget child) {
    return BlocProvider<T>._(
      key: key,
      delegate: delegate,
      child: child,
    );
  }
}
  1. Bloc使用方式一般有三種询件,
  • 通過BlocBuilder來自動(dòng)捕獲asscent的bloc,或者直接在初始化的時(shí)候傳入一個(gè)bloc,通過監(jiān)聽內(nèi)部的bloc狀態(tài),并調(diào)用setState來更新當(dāng)前的widgteTree,在BlocBuilder和它繼承的BlocBuilderBase實(shí)現(xiàn), 這里的條件主要是用來過濾Bloc的state,用于觸發(fā)不同的child widget構(gòu)建
class BlocBuilder .. extends BlocBuilderBase { ...
 
  final BlocWidgetBuilder<S> builder;

  /// {@macro blocbuilder}
  const BlocBuilder({
    Key key,
    @required this.builder,
    B bloc,
    BlocBuilderCondition<S> condition,)

  @override
  Widget build(BuildContext context, S state) => builder(context, state);
}

class _BlocBuilderBaseState<B extends Bloc<dynamic, S>, S>
    extends State<BlocBuilderBase<B, S>> { ... 
  //記錄當(dāng)前bloc的訂閱,用于在銷毀時(shí)釋放訂閱
  StreamSubscription<S> _subscription;
  //狀態(tài)值記錄,用于回調(diào)給 api調(diào)用者過濾不同的 condition,決定當(dāng)前的state是否需要觸發(fā)新的構(gòu)建
  S _previousState;
  S _state;
  B _bloc;

  @override
  void initState() {
    super.initState();
    //可以看到bloc還可以直接重BlocBuilder傳入,但這樣就不支持BlocProvider便利獲取了
    _bloc = widget.bloc ?? BlocProvider.of<B>(context);
     ...
    _subscribe();
  }

 //更新bloc的訂閱
  @override
  void didUpdateWidget(BlocBuilderBase<B, S> oldWidget) { ...
  
  //外部widget構(gòu)建的回調(diào)方法
  @override
  Widget build(BuildContext context) => widget.build(context, _state);
  
  //釋放bloc的訂閱
  @override
  void dispose() {
    _unsubscribe();
    super.dispose();
  }
  
  //監(jiān)聽bloc,并跳過intialState,因?yàn)樵趇nitial方法中已經(jīng)拿到了
  void _subscribe() {
    if (_bloc != null) {
      _subscription = _bloc.skip(1).listen((S state) {
        if (widget.condition?.call(_previousState, state) ?? true) {
          setState(() { //很熟悉的setState
            _state = state;
            _previousState = state;
            ...
}
  • 通過BlocListener來監(jiān)聽bloc的state變化,過濾指定條件的state執(zhí)行響應(yīng)的邏輯
class BlocListener<B extends Bloc<dynamic, S>, S> extends BlocListenerBase<B, S>
    with SingleChildCloneableWidget {
 
  //初始化BlocListener,指定需要過濾的條件和設(shè)置listener執(zhí)行的邏輯
  const BlocListener({
    Key key,
    @required BlocWidgetListener<S> listener,
    B bloc,
    BlocListenerCondition<S> condition,
    this.child,
  })  
  //拷貝child到當(dāng)前的BlocListener中
  @override
  BlocListener<B, S> cloneWithChild(Widget child) { ...
  @override
  Widget build(BuildContext context) => child;
}

//Listener的邏輯實(shí)現(xiàn)
class _BlocListenerBaseState<B extends Bloc<dynamic, S>, S>
    extends State<BlocListenerBase<B, S>> {
  StreamSubscription<S> _subscription;
  S _previousState;
  B _bloc;

  @override
  void initState() {
    super.initState();
    //可以自帶一個(gè)bloc缚忧,但不支持其它子widget通過BlocProvider便利獲取
    _bloc = widget.bloc ?? BlocProvider.of<B>(context);
    _previousState = _bloc?.state;
    _subscribe();
  }
  
  //更新當(dāng)前的bloc,重新訂閱
  @override
  void didUpdateWidget(BlocListenerBase<B, S> oldWidget) { ...
  //構(gòu)建child widget,這里只是by pass,沒有做任何操作
  @override
  Widget build(BuildContext context) => widget.build(context);

  //訂閱bloc state 
  void _subscribe() {
    if (_bloc != null) {
      _subscription = _bloc.skip(1).listen((S state) {
        if (widget.condition?.call(_previousState, state) ?? true) { //condition的過濾條件在這里
          widget.listener(context, state);
          _previousState = state;
        }
      });
    }
  }

  void _unsubscribe() { ...
  1. 為了便面地獄式的嵌套BlocProvider/BlocListener,引入了MultiBlocListenerMultiBlocProvider,他們屬于StatelessWidget,主要是利用一個(gè)數(shù)組存儲(chǔ)多個(gè)BlocProviderBlocListener,結(jié)合SingleChildCloneableWidget協(xié)議的實(shí)現(xiàn)
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: listeners,
      child: child,
    );
  }

// SingleChildCloneableWidget 協(xié)議的實(shí)現(xiàn)
    @override
  Widget build(BuildContext context) {
    var tree = child;
    for (final provider in providers.reversed) {
      tree = provider.cloneWithChild(tree);
    }
    return tree;
  }

  @override
  MultiProvider cloneWithChild(Widget child) {
    return MultiProvider(
      key: key,
      providers: providers,
      child: child,
    );
  }

Bloc數(shù)據(jù)緩存

主要是通過構(gòu)建中間基類,對(duì)Bloc的initialState和translation事件進(jìn)行傳遞并緩存數(shù)據(jù)到storage.可以參考HydratedBloc實(shí)現(xiàn),以Bloc官方提供的HydratedBloc為例,它主要包括HydratedBloc,Your CustomBlocBloc之間插入的基本,用于管理Bloc數(shù)據(jù)的緩存.HydratedBlocDelegate它繼承了BlocDelegate,用于替換Bloc框架內(nèi)部默認(rèn)的BlocDelegate,攔截BlocTranslation中的兩個(gè)State并存儲(chǔ)其每次變化的狀態(tài)到緩存中去.

關(guān)鍵部分的代碼實(shí)現(xiàn):

abstract class HydratedBloc<Event, State> extends Bloc<Event, State> {
  //內(nèi)置一個(gè)默認(rèn)的Storage,
  //BlocSupervisor.delegate這個(gè)是改寫之后的 BlocDelegate
  final HydratedStorage _storage =
      (BlocSupervisor.delegate as HydratedBlocDelegate).storage;
  
  //初始化時(shí)從緩存獲取數(shù)據(jù),并在子類自定義的Bloc中實(shí)現(xiàn)這個(gè)三個(gè)方法,
  @mustCallSuper
  @override
  State get initialState { ...
  State fromJson(Map<String, dynamic> json);
  Map<String, dynamic> toJson(State state);
  
  //HydratedBloc內(nèi)部使用,用于提供BlocState的key
  String get id => ''; //可以定義擴(kuò)展緩存的key名字,用于區(qū)分不同的業(yè)務(wù)邏輯數(shù)據(jù),一般公共的bloc邏輯類可以采用此類方法處理
  Future<void> clear() => _storage.delete('${runtimeType.toString()}$id'); 
}

//Bloc state攔截,和保存

class HydratedBlocDelegate extends BlocDelegate { ...
  final HydratedStorage storage; 
  static Future<HydratedBlocDelegate> build({
    Directory storageDirectory,
  }) async {
    return HydratedBlocDelegate(
      await HydratedBlocStorage.getInstance(storageDirectory: storageDirectory),
    );
  }

  /// {@macro hydratedblocdelegate}
  HydratedBlocDelegate(this.storage);
  
  //關(guān)鍵步驟
  @override
  void onTransition(Bloc bloc, Transition transition) { 
        storage.write('${bloc.runtimeType.toString()}${bloc.id}',
          json.encode(stateJson)


//這個(gè)類邏輯單一,只負(fù)責(zé)文件存儲(chǔ),提供一個(gè)存儲(chǔ)的Storage給上面的`HydratedBlocDelegate`
class HydratedBlocStorage implements HydratedStorage {...
   static Future<HydratedBlocStorage> getInstance({
    Directory storageDirectory,
   })
   Future<void> delete(String key) async {
   Future<void> delete(String key) async {
   Future<void> clear() async {

通過上面State部分的代碼可以看出,此類bloc的設(shè)計(jì)主要是對(duì)Bloc的initialState和translation攔擊來自動(dòng)保存數(shù)據(jù),默認(rèn)提供了一個(gè)key,如果有多個(gè)不同的state則需使用多個(gè)state.這種寫法其實(shí)對(duì)性能有一定影響僵控。因?yàn)樗腔跔顟B(tài)值實(shí)施存儲(chǔ)的,所以這就需要我們做一些額外的優(yōu)化痕钢。

  • 如果項(xiàng)目足夠龐大,在初始化時(shí)則會(huì)有大量的bloc緩存加載,這樣勢必會(huì)加重Flutter I/O讀取的速度,拖慢啟動(dòng)速度,而且不同的bloc之間有依賴關(guān)系,我們還需要保證他們的初始化順序完全同步,這就要求我們從業(yè)務(wù)上盡可能的對(duì)bloc的依賴層級(jí)進(jìn)行解耦,分不同的優(yōu)先等級(jí)分批初始化;

  • 另外盡量不要采用多個(gè)State狀態(tài)緩存,不管是從代碼管理,狀態(tài)傳值都會(huì)顯得非常長不便;

  • 此外為了緩解項(xiàng)目類同時(shí)過多的translation并發(fā)讀寫,可以對(duì)其進(jìn)行擴(kuò)展綁定bloc的dispose事件,標(biāo)記state之后再慢慢緩存(以內(nèi)存空間的部分緩存來解決磁盤空間的頻繁緩存)图柏。

Bloc獲取網(wǎng)絡(luò)數(shù)據(jù)

需要提前注冊(cè)Repository,注冊(cè)方式同Bloc類似,然后將Repository賦值給bloc,這樣在bloc內(nèi)部的mapEventToState的方法中就可以根據(jù)不同的event時(shí)間去訪問網(wǎng)絡(luò)請(qǐng)求了。

Bloc中的坑

  1. Bloc中所有的類型都是采用泛型推導(dǎo)的,這就要求我們?cè)谑褂玫臅r(shí)候不要忘記了指定我們它的類型.
BlocProvider<CustomBloc>.of(context);

BlocBuilder<CustomBloc,CustomBlocState>(builder: (context, state) { ...

BlocListener<CustomBloc,CustomBlocState>(listener: (preState, currentState){ ...
  1. BlocPovider作為Bloc載體,Bloc數(shù)據(jù)的存儲(chǔ)會(huì)最終通過InheritedWidget存儲(chǔ)在它當(dāng)前所注冊(cè)的作用閾中,如果我們?cè)诓煌淖饔糜蛳伦?cè)Bloc,那么它將會(huì)被低級(jí)的作用域覆蓋,因?yàn)榇藭r(shí)WidgetTree中插入了2個(gè)不同層級(jí)的InheritedWidget<CustomBloc>,
    如果想讓他們共享統(tǒng)一個(gè)bloc,就必須要保證InheritedWidget所關(guān)聯(lián)的Bloc是同一個(gè)value.根據(jù)前面對(duì)Bloc創(chuàng)建提到的2個(gè)類可以看出,Bloc創(chuàng)建分2中方法:
  • 一種是通過SingleValueDelegateState直接傳入Bloc,這種情況會(huì)將Bloc由上自下設(shè)為全部共享,但是有個(gè)弊端,會(huì)導(dǎo)致最上層的Widget移除后,bloc仍然會(huì)繼續(xù)訂閱,除非我們手動(dòng)dispose,容易存在潛在的內(nèi)存泄漏風(fēng)險(xiǎn),一般只介意在整個(gè)FlutterApp生命周期全部都需要使用的單利才可使用此方法

*另一種是通過BuilderStateDelegate來創(chuàng)建Bloc,它通過一個(gè)構(gòu)造函數(shù)create(BuildContext context) =>BlocProvider<CustomBloc>(context)`,需要注意的是后面的Bloc需要從上游已注冊(cè)的Bloc中獲取任连。

3.提到坑這里最近發(fā)掘了一個(gè)由FutureBuilder引起的bug,
在RootWidget下initialState中初始化了很多全局單例的bloc,然后在該widget的builder中使用了FutureBuilder異步構(gòu)建BlocProvider對(duì)象,BlocProvider對(duì)象蚤吹。但是在每次熱重載的時(shí)候發(fā)現(xiàn)了所有的bloc全部被管理,最后排查了Bloc的作用域,自定義實(shí)現(xiàn)BlocProvider斷點(diǎn)分析,bloc創(chuàng)建和初始化均無異常,排查FutureBuilder,它根據(jù)初始化的future生成done和其他的狀態(tài)的widget,由于是啟動(dòng)入口随抠,一般只會(huì)初始化一次,所以一開始沒太注意,但最終發(fā)現(xiàn)運(yùn)來是因?yàn)?個(gè)bugfix間接導(dǎo)致, 第一個(gè)是由于ios平臺(tái)1.17渲染異常,加上了后臺(tái)靜止切換設(shè)置setState來避免UI渲染部分缺失的問題裁着,所以在某些情況下這個(gè)FutureBuilder并不是真正的第一次創(chuàng)建,理論上來說只要這個(gè)future沒變應(yīng)該也不會(huì)出現(xiàn)另外一個(gè)rootWidget重新創(chuàng)建,然而恰哈就是因?yàn)橹安蹲絝utureBuilder傳遞的future(它是項(xiàng)目core service初始化的所有異步task的合并)在catchError的鬼使神差下,每次rootWidget構(gòu)建都會(huì)觸發(fā)futureBuilde的重新生成另外一個(gè)wiget重建,這樣所有的BlocProvider都是移除后重建,就出現(xiàn)了類似的問題.
總結(jié)之后得出2點(diǎn)結(jié)論:
FutureBuilder作用域下面

總結(jié)

Bloc框架目前已有5.6k左右,這種簡單的固定格式的寫法非常適合快速構(gòu)建項(xiàng)目架構(gòu),由于它是基于stream來實(shí)現(xiàn)的,所以使用中要特別注意訂閱的銷毀,避免出現(xiàn)內(nèi)存的泄漏,和冗余的訂閱拱她。它的數(shù)據(jù)傳遞利用了InheritedWidget的局部狀態(tài)刷新來完成層,相比使用常規(guī)的stateState來刷新獲取數(shù)據(jù)會(huì)更搞效率,這一點(diǎn)上也基本上參照了Flutter官方的設(shè)計(jì),比如ThemeData,Localization,MediaQuery等全局?jǐn)?shù)據(jù)的獲取.它局部Widget構(gòu)建BlocBuilder依然采用的setState方式構(gòu)建,因此我們需要盡量避免在BlocBuilder過多的業(yè)務(wù)邏輯處理,和不必要widget渲染,盡量把靜態(tài)的widget移出去,業(yè)務(wù)邏輯我們可以采用BlocListener對(duì)BlocBuilder包裹,來優(yōu)化代碼結(jié)構(gòu),和減少BlocBuilder的次數(shù).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末二驰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子秉沼,更是在濱河造成了極大的恐慌桶雀,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唬复,死亡現(xiàn)場離奇詭異矗积,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)敞咧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門漠魏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人妄均,你說我怎么就攤上這事柱锹。” “怎么了丰包?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵禁熏,是天一觀的道長。 經(jīng)常有香客問我邑彪,道長瞧毙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮宙彪,結(jié)果婚禮上矩动,老公的妹妹穿的比我還像新娘。我一直安慰自己释漆,他們只是感情好悲没,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著男图,像睡著了一般示姿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逊笆,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天栈戳,我揣著相機(jī)與錄音,去河邊找鬼难裆。 笑死子檀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的乃戈。 我是一名探鬼主播命锄,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼偏化!你這毒婦竟也來了脐恩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤侦讨,失蹤者是張志新(化名)和其女友劉穎驶冒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體韵卤,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骗污,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沈条。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片需忿。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蜡歹,靈堂內(nèi)的尸體忽然破棺而出屋厘,到底是詐尸還是另有隱情,我是刑警寧澤月而,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布汗洒,位于F島的核電站,受9級(jí)特大地震影響父款,放射性物質(zhì)發(fā)生泄漏溢谤。R本人自食惡果不足惜瞻凤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望世杀。 院中可真熱鬧阀参,春花似錦、人聲如沸瞻坝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湿镀。三九已至,卻和暖如春伐憾,著一層夾襖步出監(jiān)牢的瞬間勉痴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工树肃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蒸矛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓胸嘴,卻偏偏與公主長得像雏掠,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子劣像,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354