Flutter 状态管理之BLoC

在正式介绍 BLoC之前, 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。

image

但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
image

我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 StatefulWidget。但是我们很快发现,它正是造成上述原因的罪魁祸首。
State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

BLoC 是什么

旨在使用Widget更加加单,更加快捷,方便不同开发者都能使用,可以记录组件的各种状态,方便测试,让许多开发者遵循相同的模式和规则在一个代码库中无缝工作。

如何使用

简单例子

老规矩,我们写一个增加和减小的数字的例子,首先定义一个存储数据的Model,我们继承Equtable来方便与操作符==的判断,Equtable实现了使用props是否相等来判断两个对象是否相等,当然我们也可以自己重写操作符==来实现判断两个对象是否相等。

自己实现操作符如下:

  @override
  bool operator ==(Object other) {
    if (other is Model)
      return this.count == other.count &&
          age == other.count &&
          name == other.name;
    return false;
  }

使用Equtable操作符==关键代码如下:

// ignore: must_be_immutable
class Model extends Equatable {
  int count;
  int age;
  String name;
  List<String> list;
  Model({this.count = 0, this.name, this.list, this.age = 0});

  @override
  List<Object> get props => [count, name, list, age];
  Model addCount(int value) {
    return clone()..count = count + value;
  }

  Model addAge(int value) {
    return clone()..age = age + value;
  }

  Model clone() {
    return Model(count: count, name: name, list: list, age: age);
  }
}

构造一个装载Model数据的Cubit

class CounterCubit extends Cubit<Model> {
  CounterCubit() : super(Model(count: 0, name: '老王'));

  void increment() {
    print('CounterCubit +1');
    emit(state.addCount(1));
  }

  void decrement() {
    print('CounterCubit -1');
    emit(state.clone());
  }

  void addAge(int v) {
    emit(state.addAge(v));
  }

  void addCount(int v) {
    emit(state.addCount(v));
  }
}

数据准备好之后准备展示了,首先在需要展示数据小部件上层包裹一层BlocProvider,关键代码:

BlocProvider(
    create: (_) => CounterCubit(),
    child: BaseBLoCRoute(),
  )

要是多个model的话和Provider写法基本一致。

MultiBlocProvider(
      providers: [
        BlocProvider(
          create: (_) => CounterCubit(),
        ),
        BlocProvider(
          create: (_) => CounterCubit2(),
        ),
      ],
      child: BaseBLoCRoute(),
    )

然后在展示数字的widget上开始展示数据了,BlocBuilder<CounterCubit, Model>CounterCubit是载体,Model是数据,使用builder回调来刷新UI,刷新UI的条件是buildWhen: (m1, m2) => m1.count != m2.count,当条件满足时进行回调builder.

BlocBuilder<CounterCubit, Model>(
    builder: (_, count) {
      print('CounterCubit1 ');
      return Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Padding(
            child: Text(
              'count: ${count.count}',
            ),
            padding: EdgeInsets.all(20),
          ),
          OutlineButton(
            child: Icon(Icons.arrow_drop_up),
            onPressed: () {
              context.bloc<CounterCubit>().addCount(1);
            },
          ),
          OutlineButton(
            child: Icon(Icons.arrow_drop_down),
            onPressed: () {
              context.bloc<CounterCubit>().addCount(-1);
            },
          )
        ],
      );
    },
    buildWhen: (m1, m2) => m1.count != m2.count,
  )

监听状态变更

/// 监听状态变更
  void initState() {
    Bloc.observer = SimpleBlocObserver();
    super.initState();
  }
  
  
/// 观察者来观察 事件的变化 可以使用默认的 [BlocObserver]
class SimpleBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object event) {
    print(event);
    super.onEvent(bloc, event);
  }

  @override
  void onChange(Cubit cubit, Change change) {
    print(change);
    super.onChange(cubit, change);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    print(transition);
    super.onTransition(bloc, transition);
  }

  @override
  void onError(Cubit cubit, Object error, StackTrace stackTrace) {
    print(error);
    super.onError(cubit, error, stackTrace);
  }
}
image

局部刷新

布局刷新是使用BlocBuilder来实现的,BlocBuilder<CounterCubit, Model>CounterCubit是载体,Model是数据,使用builder回调来刷新UI,刷新UI的条件是buildWhen: (m1, m2) => m1.count != m2.count,当条件满足时进行回调builder.
本例子是多个model,多个局部UI刷新

  Widget _body() {
    return Center(
      child: CustomScrollView(
        slivers: <Widget>[
          SliverToBoxAdapter(
            child: BlocBuilder<CounterCubit, Model>(
              builder: (_, count) {
                print('CounterCubit1 ');
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Padding(
                      child: Text(
                        'count: ${count.count}',
                      ),
                      padding: EdgeInsets.all(20),
                    ),
                    OutlineButton(
                      child: Icon(Icons.arrow_drop_up),
                      onPressed: () {
                        context.bloc<CounterCubit>().addCount(1);
                      },
                    ),
                    OutlineButton(
                      child: Icon(Icons.arrow_drop_down),
                      onPressed: () {
                        context.bloc<CounterCubit>().addCount(-1);
                      },
                    )
                  ],
                );
              },
              buildWhen: (m1, m2) => m1.count != m2.count,
            ),
          ),
          SliverToBoxAdapter(
            child: SizedBox(
              height: 50,
            ),
          ),
          SliverToBoxAdapter(
            child: BlocBuilder<CounterCubit, Model>(
              builder: (_, count) {
                print('CounterCubit age build ');
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Padding(
                      child: Text(
                        'age:${count.age}',
                      ),
                      padding: EdgeInsets.all(20),
                    ),
                    OutlineButton(
                      child: Icon(Icons.arrow_drop_up),
                      onPressed: () {
                        context.bloc<CounterCubit>().addAge(1);
                      },
                    ),
                    OutlineButton(
                      child: Icon(Icons.arrow_drop_down),
                      onPressed: () {
                        context.bloc<CounterCubit>().addAge(-1);
                      },
                    )
                  ],
                );
              },
              buildWhen: (m1, m2) => m1.age != m2.age,
            ),
          ),
          SliverToBoxAdapter(
            child: BlocBuilder<CounterCubit2, Model>(
              builder: (_, count) {
                print('CounterCubit2 ');
                return Column(
                  children: <Widget>[
                    Text('CounterCubit2: ${count.age}'),
                    OutlineButton(
                      child: Icon(Icons.add),
                      onPressed: () {
                        context.bloc<CounterCubit2>().addAge(1);
                      },
                    )
                  ],
                );
              },
            ),
          )
        ],
      ),
    );
  }
image

当我们点击加好或者减号已经被SimpleBlocObserver监听到,看下打印信息,每次model变更都会通知监听者。

flutter: Change { currentState: Model, nextState: Model }
flutter: CounterCubit2
flutter: Change { currentState: Model, nextState: Model }
flutter: CounterCubit2

复杂状态变更,监听和刷新UI

一个加减例子,每次加减我们在当前组件中监听,当状态变更的时候如何实现刷新UI,而且当age+count == 10的话返回上一页。

要满足此功能的话,同一个部件至少要listenerbuilder,正好官方提供的BlocConsumer可以实现,如果只需要监听则需要使用BlocListener,简单来说是BlocConsumer=BlocListener+BlocBuilder.

看关键代码:

BlocConsumer<CounterCubit, Model>(builder: (ctx, state) {
  return Column(
    children: <Widget>[
      Text(
          'age:${context.bloc<CounterCubit>().state.age} count:${context.bloc<CounterCubit>().state.count}'),
      OutlineButton(
        child: Text('age+1'),
        onPressed: () {
          context.bloc<CounterCubit>().addAge(1);
        },
      ),
      OutlineButton(
        child: Text('age-1'),
        onPressed: () {
          context.bloc<CounterCubit>().addAge(-1);
        },
      ),
      OutlineButton(
        child: Text('count+1'),
        onPressed: () {
          context.bloc<CounterCubit>().addCount(1);
        },
      ),
      OutlineButton(
        child: Text('count-1'),
        onPressed: () {
          context.bloc<CounterCubit>().addCount(-1);
        },
      )
    ],
  );
}, listener: (ctx, state) {
              if (state.age + state.count == 10)                Navigator.maybePop(context);
})

效果如下:

image

复杂情况(Cubit)

登陆功能(继承 Cubit)

我们再编写一个完整登陆功能,分别用到BlocListener用来监听是否可以提交数据,用到BlocBuilder用来刷新UI,名字输入框和密码输入框分别用BlocBuilder包裹,实现局部刷新,提交按钮用BlocBuilder包裹用来展示可用和不可用状态。

此为bloc_login的官方例子的简单版本,想要了解更多请查看官方版本

观察者

观察者其实一个APP只需要写一次即可,一般在APP初始化配置即可。
我们这里只提供打印状态变更信息。

class DefaultBlocObserver extends BlocObserver {
  @override
  void onChange(Cubit cubit, Change change) {
    if (kDebugMode)
      print(
          '${cubit.toString()} new:${change.toString()} old:${cubit.state.toString()}');
    super.onChange(cubit, change);
  }
}

在初始化指定观察者

@override
void initState() {
  Bloc.observer=DefaultBlocObserver();
  super.initState();
}

或者使用默认观察者

Bloc.observer = BlocObserver();

State(Model)

存储数据的state(Model),这里我们需要账户信息,密码信息,是否可以点击登录按钮,是否正在登录这些信息。

enum LoginState {
  success,
  faild,
  isLoading,
}
enum BtnState { available, unAvailable }

class LoginModel extends Equatable {
  final String name;
  final String password;
  final LoginState state;
  LoginModel({this.name, this.password, this.state});
  @override
  List<Object> get props => [name, password, state, btnVisiable];
  LoginModel copyWith({String name, String pwd, LoginState loginState}) {
    return LoginModel(
        name: name ?? this.name,
        password: pwd ?? this.password,
        state: loginState ?? this.state);
  }

  bool get btnVisiable =>
      (password?.isNotEmpty ?? false) && (name?.isNotEmpty ?? false);
  @override
  String toString() {
    return '$props';
  }
}

Cubit

装载state的类,当state变更需要调用emit(state),state的变更条件是==,所以我们上边的state(Model)继承了Equatable,Equatable内部实现了操作符==函数,我们只需要将它所需props重写即可。

class LoginCubit extends Cubit<LoginModel> {
  LoginCubit(state) : super(state);
  void login() async {
    emit(state.copyWith(loginState: LoginState.isLoading));
    await Future.delayed(Duration(seconds: 2));
    if (state.btnVisiable == true)
      emit(state.copyWith(loginState: LoginState.success));
    emit(state.copyWith(loginState: LoginState.faild));
  }

  void logOut() async {
    emit(state.copyWith(
      name: null,
      pwd: null,
    ));
  }

  void changeName({String name}) {
    emit(state.copyWith(
        name: name, pwd: state.password, loginState: state.state));
  }

  void changePassword({String pwd}) {
    emit(state.copyWith(name: state.name, pwd: pwd, loginState: state.state));
  }
}

构造view

关键还是得看如何构造UI,首先输入框分别使用BlocBuilder包裹实现局部刷新,局部刷新的关键还是buildWhen得写的漂亮,密码输入框的话只需要判断密码是否改变即可,账号的话只需要判断账号是否发生改变即可,
按钮也是如此,在UI外层使用listener来监听状态变更,取所需要的状态跳转新的页面或者弹窗。

首先看下输入框关键代码:

class TextFiledNameRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginCubit, LoginModel>(
        builder: (BuildContext context, LoginModel state) {
          return TextField(
            onChanged: (v) {
              context.bloc<LoginCubit>().changeName(name: v);
            },
            decoration: InputDecoration(
                labelText: 'name',
                errorText: state.name?.isEmpty ?? false ? 'name不可用' : null),
          );
        },
        buildWhen: (previos, current) => previos.name != current.name);
  }
}

class TextFiledPasswordRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginCubit, LoginModel>(
        builder: (BuildContext context, LoginModel state) {
          return TextField(
            onChanged: (v) {
              context.bloc<LoginCubit>().changePassword(pwd: v);
            },
            decoration: InputDecoration(
                labelText: 'password',
                errorText:
                    state.password?.isEmpty ?? false ? 'password不可用' : null),
          );
        },
        buildWhen: (previos, current) => previos.password != current.password);
  }
}

按钮根据不同的状态来显示可用或不可用或正在提交的动画效果。

class LoginButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginCubit, LoginModel>(
        builder: (BuildContext context, LoginModel state) {
          switch (state.state) {
            case LoginState.isLoading:
              return const CircularProgressIndicator();
            default:
              return RaisedButton(
                child: const Text('login'),
                onPressed: state.btnVisiable
                    ? () {
                        context.bloc<LoginCubit>().login();
                      }
                    : null,
              );
          }
        },
        buildWhen: (previos, current) =>
            previos.btnVisiable != current.btnVisiable ||
            (current.state != previos.state));
  }
}

小部件写好了,那么我们将他们组合起来

class BaseLoginPageRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => LoginCubit(LoginModel()),
      child: BaseLoginPage(),
    );
  }

  static String routeName = '/BaseLoginPageRoute';
  MaterialPageRoute get route =>
      MaterialPageRoute(builder: (_) => BaseLoginPageRoute());
}

class BaseLoginPage extends StatefulWidget {
  BaseLoginPage({Key key}) : super(key: key);

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

class _BaseLoginPageState extends State<BaseLoginPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('loginBLoC Cubit'),
      ),
      body: _body(),
    );
  }

  Widget _body() {
    return BlocListener<LoginCubit, LoginModel>(
      listener: (context, state) {
        if (state.state == LoginState.success) {
          Scaffold.of(context)
            ..hideCurrentSnackBar()
            ..showSnackBar(const SnackBar(content: Text('登陆成功')));
        }
      },
      child: Center(
        child: Column(
          children: <Widget>[
            TextFiledNameRoute(),
            TextFiledPasswordRoute(),
            const SizedBox(
              height: 20,
            ),
            LoginButton()
          ],
        ),
      ),
    );
  }

  @override
  void initState() {
    Bloc.observer = BlocObserver();
    super.initState();
  }
}

这里我们实现了登陆成功弹出snackBar.

看下效果图哦:

image

复杂情况(Bloc)

情况1都我们手动emit(state),那么有没有使用流技术来直接监听的呢?答案是有,那么我们再实现一遍使用bloc的登陆功能。

state(数据载体)

首先我们使用 一个抽象类来定义事件,然后各种小的事件都继承它,比如:NameEvent装载了姓名信息,PasswordEvent装载了密码信息,SubmittedEvent装载了提交信息,简单来讲,event就是每一个按钮点击事件或者valueChange事件触发的动作,最好下载代码之后自己对比下,然后自己从简单例子写,此为稍微复杂情况,看下关键代码:

/// 登陆相关的事件
abstract class LoginEvent extends Equatable {
  const LoginEvent();
  @override
  List<Object> get props => [];
}

/// 修改密码
class LoginChagnePassword extends LoginEvent {
  final String password;
  const LoginChagnePassword({this.password});
  @override
  List<Object> get props => [password];
}

/// 修改账户
class LoginChagneName extends LoginEvent {
  final String name;
  const LoginChagneName({this.name});
  @override
  List<Object> get props => [name];
}

/// 提交事件
class LoginSubmitted extends LoginEvent {
  const LoginSubmitted();
  @override
  List<Object> get props => [];
}

存储数据的state,在LoginBloc中将event转换成state,那么state需要存储什么数据呢?需要存储账户信息、密码、登陆状态等信息。

/// 事件变更状态[正在请求,报错,登陆成功,初始化]
enum Login2Progress { isRequesting, error, success, init }

/// 存储数据的model 在[bloc]中称作[state]
class LoginState2 extends Equatable {
  final String name;
  final String password;
  final Login2Progress progress;
  LoginState2({this.name, this.password, this.progress = Login2Progress.init});
  @override
  List<Object> get props => [name, password, btnVisiable, progress];
  LoginState2 copyWith(
      {String name, String pwd, Login2Progress login2progress}) {
    return LoginState2(
        name: name ?? this.name,
        password: pwd ?? this.password,
        progress: login2progress ?? this.progress);
  }

  /// 使用 [UserName] &&[UserPassword]来校验规则
  bool get btnVisiable => nameVisiable && passwordVisiable;
  bool get nameVisiable => UserName(name).visiable;
  bool get passwordVisiable => UserPassword(password).visiable;

  /// 是否展示名字错误信息

  bool get showNameErrorText {
    if (name?.isEmpty ?? true) return false;
    return nameVisiable == false;
  }

  /// 是否展示密码错误信息
  bool get showPasswordErrorText {
    if (password?.isEmpty ?? true) return false;
    return passwordVisiable == false;
  }

  @override
  String toString() {
    return '$props';
  }
}

eventstate写好了,怎么将event转换成state呢?首先新建一个类继承Bloc,覆盖函数mapEventToState,利用这个函数参数event来对state,进行转换,中间因为用到了虚拟的网络登陆,耗时操作和状态变更,所以使用了yield*返回了另外一个流函数。

class LoginBloc extends Bloc<LoginEvent, LoginState2> {
  LoginBloc(initialState) : super(initialState);

  @override
  Stream<LoginState2> mapEventToState(event) async* {
    if (event is LoginChagneName) {
      yield _mapChangeUserNameToState(event, state);
    } else if (event is LoginChagnePassword) {
      yield _mapChangePasswordToState(event, state);
    } else if (event is LoginSubmitted) {
      yield* _mapSubmittedToState(event, state);
    }
  }
 /// 改变密码
  LoginState2 _mapChangePasswordToState(
      LoginChagnePassword event, LoginState2 state2) {
    return state2.copyWith(pwd: event.password ?? '');
  }

  /// 改变名字
  LoginState2 _mapChangeUserNameToState(
      LoginChagneName event, LoginState2 state2) {
    return state2.copyWith(name: event.name ?? '');
  }

  /// 提交
  Stream<LoginState2> _mapSubmittedToState(
      LoginSubmitted event, LoginState2 state2) async* {
    try {
      if (state2.name.isNotEmpty && state2.password.isNotEmpty) {
        yield state2.copyWith(login2progress: Login2Progress.isRequesting);
        await Future.delayed(Duration(seconds: 2));
        yield state2.copyWith(login2progress: Login2Progress.success);

        yield state2.copyWith(login2progress: Login2Progress.init);
      }
    } on Exception catch (e) {
      yield state2.copyWith(login2progress: Login2Progress.error);
    }
  }
}

stateevent事件整理成图方便理解一下:

image

构造view

样式我们还是使用上边的 ,但是发送事件却不一样,原因是继承bloc其实是实现了EventSink的接口,使用add()触发监听。

class TextFiledNameRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginBloc, LoginState2>(
        builder: (BuildContext context, LoginState2 state) {
          return TextField(
            onChanged: (v) {
              context.bloc<LoginBloc>().add(LoginChagneName(name: v));
            },
            textAlign: TextAlign.center,
            decoration: InputDecoration(
                labelText: 'name',
                errorText:
                    (state.showNameErrorText == true) ? 'name不可用' : null),
          );
        },
        buildWhen: (previos, current) => previos.name != current.name);
  }
}

完整的效果是:

image

BLoC 流程

首先view部件持有CubitCubit持有状态(Model),当状态(Model)发生变更时通知Cubit,Cubit依次通知listenerBlocBulder.builder进行刷新UI,每次状态变更都会通知BlocObserver,可以做到全局的状态监听。

千言万语不如一张图:

image

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271