Flutter GetX looks 因吹斯听

前言

get | Flutter Package (flutter-io.cn) 一直是 Flutter 中带有争议的一个三方库。正是因为有争议,所以我们应该有自己的判断,无需站队。

一方面

它是 pub.dev 中点赞第一的库

[图片上传失败...(image-d10e8-1639806641181)]

Github Star 数量超过 5500

[图片上传失败...(image-332ca1-1639806641182)]

拥有 140+ 的贡献者

[图片上传失败...(image-82d49d-1639806641182)]

1400+ 的 Issue
[图片上传失败...(image-e28aac-1639806641182)]

这些都在说明,这是一个热度很高的三方组件库。

另一方面

它也是开发者吐槽的对象。

[图片上传失败...(image-a3b865-1639806641182)]

[图片上传失败...(image-c5fa6d-1639806641182)]

[图片上传失败...(image-d52af1-1639806641182)]

[图片上传失败...(image-677c48-1639806641182)]

可以看到,槽点还是满满的,我们暂时按下不表,先了解下什么是 GetX

正题

GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理 - 来自官方的描述。

官方文档介绍的三大功能也是如此。

[图片上传失败...(image-1727d1-1639806641182)]

我们下载一下 GetX 项目,打开看看结构是什么样子的。

  • 从文件夹上面可以大概看出来每个部分负责的功能.

get_connect: 网络相关

get_instance: 注入相关

get_navigation: 路由相关

get_rx: 魔法相关(狗头)

get_state_manager: 状态相关

[图片上传失败...(image-801795-1639806641182)]

  • 不得不说,支持多个国家的文档,这是很赞的事情。当然,这是对于那些会看文档的人来说。

[图片上传失败...(image-c176bc-1639806641182)]

接下来我将从源码的角度,分析一下 GetX 的三大功能。

依赖管理

依赖管理 提到到前面来讲,因为其他2个功能或多或少都基于它。

定义类

大部分情况下,这个类需要去继承 GetxController,以便于整个系统自动为它做
dispose 的操作(这部分会在路由管理中讲)。

class FFController extends GetxController {}

注册

// 普通方式
Get.put<FFController>(FFController());
// 如果你想这个实例永远存在,不被删除,可以把 permanent 设置为 true
Get.put<FFController>(FFController(), permanent: true);
// 如果你的场景中,会存在多个相同的 FFController 实例,你可以用 tag 来进行区分
Get.put<FFController>(FFController(), tag: 'unique key');
// 使用的时候才创建类
Get.lazyPut<FFController>(() => FFController());
// 注册一个异步实例
Get.putAsync<FFController>(() async => FFController());

获取

// 普通方式
FFController controller = Get.find<FFController>();
// 如果你的场景中,会存在多个相同的 FFController 实例,你可以用 tag 来进行区分
FFController controller = Get.find<FFController>(tag: 'unique key');

原理

实际上,你跟代码进入 Get.put 或者 Get.find, 最终都指向 GetInstance

GetInstance 其实就是一个单例(Dart 单线程真香?),它利用一个 _singl Map 存储着你注册的对象/工厂方法,具体的过程不表。

class GetInstance {
  factory GetInstance() => _getInstance ??= GetInstance._();

  const GetInstance._();

  static GetInstance? _getInstance;

  T call<T>() => find<T>();

  /// Holds references to every registered Instance when using
  /// `Get.put()`
  static final Map<String, _InstanceBuilderFactory> _singl = {};

  /// Holds a reference to every registered callback when using
  /// `Get.lazyPut()`
  // static final Map<String, _Lazy> _factory = {};
}

状态管理

[图片上传失败...(image-1da0ea-1639806641182)]

在讲这一部分的之前,再次重申下,框架再怎么骚操作,最终都会回归到
setState(() {});

Obx

这是 GetX 当中最大的一个魔法,我们先看看它是怎么用的。

obs

我们先在 FFController 当中增加一个 <int>[] 数组变量,obs 是一个扩展方法,它将返回 RxList<int>,至于什么是 RxList,我们这里暂时不深入,先看看是怎么使用的。

class FFController extends GetxController {
  RxList<int> list = <int>[].obs;
}
Obx

使用 Obx 包含需要更新状态的部分,点击 Icons.add 按钮,你会发生整个列表发生改变。

class RxListDemo extends StatelessWidget {
  const RxListDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    FFController controller = Get.put<FFController>(FFController());
    return Scaffold(
      appBar: AppBar(),
      body: Obx(
        () {
          return ListView.builder(
            itemBuilder: (BuildContext b, int index) {
              return Text('$index:${controller.list[index]}');
            },
            itemCount: controller.list.length,
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),      
        onPressed: () {
          controller.list.add(Random().nextInt(100));
        },
      ),
    );
  }
}
原理

首先,看看 RxList 是什么东西。这里只放上一部分代码,可以看到 RxList
对于 List 所以的方法和操作都做了 override,并且去调用 refresh 方法。

  @override
  void operator []=(int index, E val) {
    _value[index] = val;
    refresh();
  }

  /// Special override to push() element(s) in a reactive way
  /// inside the List,
  @override
  RxList<E> operator +(Iterable<E> val) {
    addAll(val);
    refresh();
    return this;
  }

  @override
  E operator [](int index) {
    return value[index];
  }

  @override
  void add(E item) {
    _value.add(item);
    refresh();
  }

refresh 中是去执行了 Stream.add 方法。那么 Stream 是谁在消费呢?

  GetStream<T> subject = GetStream<T>();
  final _subscriptions = <GetStream, List<StreamSubscription>>{};
  void refresh() {
    subject.add(value);
  }

我们来看看 Obx 里面藏着什么。

class Obx extends ObxWidget {
  final WidgetCallback builder;

  const Obx(this.builder);

  @override
  Widget build() => builder();
}

Obx 继承于 ObxWidgetObxWidget 是一个 StatefulWidget,在 _ObxState 初始化的时候 _observer 做了监听,当它被通知的时候会触发
_updateTree ,也就是我们常见的 setState(() {});

abstract class ObxWidget extends StatefulWidget {
  const ObxWidget({Key? key}) : super(key: key);

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties..add(ObjectFlagProperty<Function>.has('builder', build));
  }

  @override
  _ObxState createState() => _ObxState();

  @protected
  Widget build();
}

class _ObxState extends State<ObxWidget> {
  final _observer = RxNotifier();
  late StreamSubscription subs;

  @override
  void initState() {
    super.initState();
    subs = _observer.listen(_updateTree, cancelOnError: false);
  }

  void _updateTree(_) {
    if (mounted) {
      setState(() {});
    }
  }

  @override
  void dispose() {
    subs.cancel();
    _observer.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) =>
      RxInterface.notifyChildren(_observer, widget.build);
}

而在 RxInterface.notifyChildren 方法中将 _observer 传递进去。其实我们可以看到这个方法只做了一件事情,在 builder 回调执行之前,设置 RxInterface.proxy 为当前 _ObxState 中的 _observer

  /// Avoids an unsafe usage of the `proxy`
  static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
    final _observer = RxInterface.proxy;
    RxInterface.proxy = observer;
    final result = builder();
    if (!observer.canUpdate) {
      RxInterface.proxy = _observer;
      throw """
      [Get] the improper use of a GetX has been detected. 
      You should only use GetX or Obx for the specific widget that will be updated.
      If you are seeing this error, you probably did not insert any observable variables into GetX/Obx 
      or insert them outside the scope that GetX considers suitable for an update 
      (example: GetX => HeavyWidget => variableObservable).
      If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX.
      """;
    }
    RxInterface.proxy = _observer;
    return result;
  }

而在 builder 方法中当 controller.list[index]controller.list.length 被调用的时候。

return ListView.builder(
   itemBuilder: (BuildContext b, int index) {
      return Text('$index:${controller.list[index]}');
   },
   itemCount: controller.list.length,
);

会执行 RxInterface.proxy?.addListener(subject); ,就将神奇的 RxListObx 关联起来了。

  @override
  E operator [](int index) {
    return value[index];
  }

  @override
  int get length => value.length;

  @override
  @protected
  List<E> get value {
    RxInterface.proxy?.addListener(subject);
    return _value;
  }

接下来我们看看 debug 的堆栈信息,就能很清楚整个流程的运作方式了。

  • 创建监听

[图片上传失败...(image-336b63-1639806641182)]

  • RxInterface.proxy 设置为当前 _observer
    [图片上传失败...(image-1ae901-1639806641182)]

  • builder 回调中,即将触发 RxList 的神器魔法

[图片上传失败...(image-296e6c-1639806641182)]

  • 去订阅 RxList 中的 Stream
    [图片上传失败...(image-876ddf-1639806641182)]

  • 正式监听
    [图片上传失败...(image-b0cb7c-1639806641182)]

  • 当我们对 RxList 进行改变,比如 add 的时候,触发监听

[图片上传失败...(image-29c5ce-1639806641182)]

  • 最终触发 _ObxState 中的 _updateTree

[图片上传失败...(image-c6ffca-1639806641182)]

  • Obx dispose 的时候关闭流。
  @override
  void dispose() {
    subs.cancel();
    _observer.close();
    super.dispose();
  }
小结
  • .obs 系列,包含对基础的 int,double,List 等基础结构的封装,并且包含了一个 Stream 来做通知。

  • Obx 通过对 RxInterface.proxy 的设置(该死的 Dart 单线程,真香! ),确保 builder 回调中的 .obs 只关联当前的 RxInterface.proxy=》Obx,来确保当前 .obs 只会触发对应 Obx 的刷新。

  • 你不需要创建 SreamController;你不需要为每个变量创建一个 StreamBuilder;你不需要为每个变量创建 ValueNotifier ... 有一说一,真香。

[图片上传失败...(image-7562c3-1639806641182)]

GetxController

往往跟 GetBuilder 一起使用,跟 ChangeNotifier 相似。

class FFController extends GetxController {
  List<int> list = <int>[];
  void add(int i) {
    list.add(i);
    update();
  }
}

class RxListDemo extends StatelessWidget {
  RxListDemo({Key? key}) : super(key: key);
  FFController controller = Get.put<FFController>(FFController());
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: GetBuilder<FFController>(
        builder: (FFController controller) {
          return ListView.builder(
            itemBuilder: (BuildContext b, int index) {
              return Text('$index:${controller.list[index]}');
            },
            itemCount: controller.list.length,
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          controller.add(Random().nextInt(100));
        },
      ),
    );
  }
}

主要核心代码不多,原理简单讲下,利用 GetInstanceFFController 做监听,等 FFController update 的时候刷新 GetBuilder。在 dispose 的时候跟进条件释放 FFController

class GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
    with GetStateUpdaterMixin {
  T? controller;
  bool? _isCreator = false;
  VoidCallback? _remove;
  Object? _filter;

  @override
  void initState() {
    // _GetBuilderState._currentState = this;
    super.initState();
    widget.initState?.call(this);

    var isRegistered = GetInstance().isRegistered<T>(tag: widget.tag);

    if (widget.global) {
      if (isRegistered) {
        if (GetInstance().isPrepared<T>(tag: widget.tag)) {
          _isCreator = true;
        } else {
          _isCreator = false;
        }
        controller = GetInstance().find<T>(tag: widget.tag);
      } else {
        controller = widget.init;
        _isCreator = true;
        GetInstance().put<T>(controller!, tag: widget.tag);
      }
    } else {
      controller = widget.init;
      _isCreator = true;
      controller?.onStart();
    }

    if (widget.filter != null) {
      _filter = widget.filter!(controller!);
    }

    _subscribeToController();
  }

  /// Register to listen Controller's events.
  /// It gets a reference to the remove() callback, to delete the
  /// setState "link" from the Controller.
  void _subscribeToController() {
    _remove?.call();
    _remove = (widget.id == null)
        ? controller?.addListener(
            _filter != null ? _filterUpdate : getUpdate,
          )
        : controller?.addListenerId(
            widget.id,
            _filter != null ? _filterUpdate : getUpdate,
          );
  }

  void _filterUpdate() {
    var newFilter = widget.filter!(controller!);
    if (newFilter != _filter) {
      _filter = newFilter;
      getUpdate();
    }
  }

  @override
  void dispose() {
    super.dispose();
    widget.dispose?.call(this);
    if (_isCreator! || widget.assignId) {
      if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
        GetInstance().delete<T>(tag: widget.tag);
      }
    }

    _remove?.call();

    controller = null;
    _isCreator = null;
    _remove = null;
    _filter = null;
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    widget.didChangeDependencies?.call(this);
  }

  @override
  void didUpdateWidget(GetBuilder oldWidget) {
    super.didUpdateWidget(oldWidget as GetBuilder<T>);
    // to avoid conflicts when modifying a "grouped" id list.
    if (oldWidget.id != widget.id) {
      _subscribeToController();
    }
    widget.didUpdateWidget?.call(oldWidget, this);
  }

  @override
  Widget build(BuildContext context) {
    // return _InheritedGetxController<T>(
    //   model: controller,
    //   child: widget.builder(controller),
    // );
    return widget.builder(controller!);
  }
}

GetxController 和一些保存在 GetInstance 中的对象的自动释放,又跟我们 GexX 的路由管理息息相关。

路由管理

Flutter 中的 context 是很重要的东西,很多 api 都是离不开它的。你一定会有过这种想法,希望在没有 context 的情况下使用路由,SnackBars , Dialogs , BottomSheets .

实际上,无 context 路由的方法其实是很简单。

class App extends StatefulWidget {
  const App({Key? key}) : super(key: key);
  static final GlobalKey<NavigatorState> navigatorKey =
      GlobalKey(debugLabel: 'navigate');
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: App.navigatorKey,
      home: RxListDemo(),
    );
  }
}

使用的时候你只需要

App.navigatorKey.currentState.pushNamed('/home');

而这一切 GexX 都为你封装好了,你只需要将 MaterialApp 换成 GetMaterialApp

GetMaterialApp( // Before: MaterialApp(
  home: MyHome(),
)

使用的时候你只需要这样

Get.to(NextScreen());
Get.back();
Get.back(result: 'success');
Get.toNamed("/NextScreen");
Get.toNamed("/NextScreen", arguments: 'Get is the best');
// 获取参数
print(Get.arguments);

当然,GexX 的路由,远远不只你看到的这些,它更多的任务是串联起了整个 GexX 宇宙。

GetPage

GetPage 继承于 Page<T> ,而 Page<T> 继承于 RouteSettings. 它是对一个页面的描述。通过 GetPage 组装成 GetPageRoute

    GetMaterialApp(
      initialRoute: '/',
      getPages: [
      GetPage(
        name: '/',
        page: () => MyHomePage(),
      ),
      GetPage(
        name: '/profile/',
        page: () => MyProfile(),
      ),
     ],
    )

GetPageRoute

MaterialPageRouteCupertinoPageRoute 大家都应该很熟悉,GetPageRoute 和它们是一个东西。

class GetPageRoute<T> extends PageRoute<T>
    with GetPageRouteTransitionMixin<T>, PageRouteReportMixin {
}

不同的是它还有其他任务,它会在 install(你可以简单理解为 push ) 和 dispose(你可以简单理解为 pop ) 的时候去通知 RouterReportManager

mixin PageRouteReportMixin<T> on Route<T> {
  @override
  void install() {
    super.install();
    RouterReportManager.reportCurrentRoute(this);
  }

  @override
  void dispose() {
    super.dispose();
    RouterReportManager.reportRouteDispose(this);
  }
}

RouterReportManager 的任务之一就是去管理我们在当前页面注册的各种实例,下面为部分重要的代码。

class RouterReportManager<T> {
  static final Map<Route?, List<String>> _routesKey = {};

  static final Map<Route?, HashSet<Function>> _routesByCreate = {};

  static Route? _current;

  // ignore: use_setters_to_change_properties
  static void reportCurrentRoute(Route newRoute) {
    _current = newRoute;
  }

  /// Links a Class instance [S] (or [tag]) to the current route.
  /// Requires usage of `GetMaterialApp`.
  static void reportDependencyLinkedToRoute(String depedencyKey) {
    if (_current == null) return;
    if (_routesKey.containsKey(_current)) {
      _routesKey[_current!]!.add(depedencyKey);
    } else {
      _routesKey[_current] = <String>[depedencyKey];
    }
  }

  static void reportRouteDispose(Route disposed) {
    if (Get.smartManagement != SmartManagement.onlyBuilder) {
      WidgetsBinding.instance!.addPostFrameCallback((_) {
        _removeDependencyByRoute(disposed);
      });
    }
  }
  • push 新页面触发 reportCurrentRoute,设置当前 _current
  • 当在当前页面调用 Get.put 的时候会调用到 reportDependencyLinkedToRoute 方法,保存起来。
  • pop 页面的时候触发 reportRouteDispose 根据一些规则,释放掉实例。

FFRoute

在实际使用中,下面 2 点是我不能习惯的。

  • 手动去设置 getPages 集合
  • 由于只能通过 Get.arguments 获取参数,弱类型让人很不舒服。

为此我特意写增加了 FFRouteGetX 结合的例子。(FFRoute 是一个利用注解生成路由的工具)

ff_annotation_route/example_getx at master · fluttercandies/ff_annotation_route (github.com)

  • 实际上,你只是需要在 onGenerateRoute 回调中将 FFRouteSettings 转为为对应的 GetPageRoute
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'ff_annotation_route demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: Routes.fluttercandiesMainpage.name,
      onGenerateRoute: (RouteSettings settings) {
        FFRouteSettings ffRouteSettings = getRouteSettings(
          name: settings.name!,
          arguments: settings.arguments as Map<String, dynamic>?,
          notFoundPageBuilder: () => Scaffold(
            appBar: AppBar(),
            body: const Center(
              child: Text('not find page'),
            ),
          ),
        );
        Bindings? binding;
        if (ffRouteSettings.codes != null) {
          binding = ffRouteSettings.codes!['binding'] as Bindings?;
        }

        Transition? transition;
        bool opaque = true;
        if (ffRouteSettings.pageRouteType != null) {
          switch (ffRouteSettings.pageRouteType) {
            case PageRouteType.cupertino:
              transition = Transition.cupertino;
              break;
            case PageRouteType.material:
              transition = Transition.downToUp;
              break;
            case PageRouteType.transparent:
              opaque = false;
              break;
            default:
          }
        }

        return GetPageRoute(
          binding: binding,
          opaque: opaque,
          settings: ffRouteSettings,
          transition: transition,
          page: () => ffRouteSettings.builder(),
        );
      },
    );
  }
}
  • 使用的时候这样写

Get.toNamed(Routes.itemPage.name,arguments: Routes.itemPage.d(index: index));

总结

这不是一篇介绍如何使用 GetX 的文章,只是从源码的角度来简单地理解 GetX 三大功能的原理,仅此而已。

优点

  • 使用简单

    如果你对 Flutter 的原理有所理解,GetX 绝对是大杀器,它能大大减少你编写代码的时间。

  • 功能丰富

    除了状态管理,依赖管理,路由管理三大功能,它还包含国际化,主题,网络请求等,有一种全家桶的感觉。

缺点

  • 使用简单

    这是它的优点也是它的缺点。它隐藏了 Flutter 最基础的原理。新手用起来可能很爽,但是如果遇到问题很难去排查。很明显的现象就是会有很多新手到群里问,GetX 怎么不起作用了,时间长了,确实很让人沮丧。

  • 功能丰富

    太多的封装,让人不得不考虑到,如果这个库停止更新了,会有多大的影响。尽管官方作出以下的承诺,但我想 always 这个词应该是慎用的。

[图片上传失败...(image-aca11a-1639806641182)]

  • 过于夸张的描述

    一些描述过于浮夸,这也是导致 GetXFlutter Team 取消掉 Flutter Favorite 的原因之一。

结语

GetX 是一个现象级的三方库,如何使用它,完全根据你自身的情况。建议新手不要上来就使用三方框架,它们会阻碍你对 Flutter 原理的理解。实际上,技术往往没有什么错误,只是使用的人不一样而已。

最后放上 GetX 官方中文文档:

  1. README

  2. 依赖管理

  3. 状态管理

  4. 路由管理

Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果[图片上传失败...(image-a3b79c-1639806641182)]QQ群:181398081

最最后放上 Flutter Candies 全家桶,真香。

[图片上传失败...(image-fa704b-1639806641182)]

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

推荐阅读更多精彩内容