Flutter GetX looks 因吹斯听

10,821 阅读10分钟

前言

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

一方面

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

截屏2021-12-10 下午5.05.40.png

Github Star 数量超过 5500

截屏2021-12-10 下午5.06.22.png

拥有 140+ 的贡献者

截屏2021-12-10 下午5.06.50.png

1400+ 的 Issue 截屏2021-12-10 下午5.12.39.png

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

另一方面

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

截屏2021-12-10 下午5.14.23.png

A36E6E28-D475-4387-90C0-B28FBF384365.png

getx.jpg

B7326A01E7479C435A7AB79F703227CF.jpg

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

正题

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

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

880D7581-9132-472C-8C53-FD9EF929DD27.png

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

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

get_connect: 网络相关

get_instance: 注入相关

get_navigation: 路由相关

get_rx: 魔法相关(狗头)

get_state_manager: 状态相关

截屏2021-12-11 上午9.57.59.png

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

截屏2021-12-11 上午9.40.23.png

接下来我将从源码的角度,分析一下 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 = {};
}

状态管理

E0655AB2-864E-415F-8C6D-24B3ECF241C7.png

在讲这一部分的之前,再次重申下,框架再怎么骚操作,最终都会回归到 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 的堆栈信息,就能很清楚整个流程的运作方式了。

  • 创建监听

截屏2021-12-11 下午1.31.15.png

  • RxInterface.proxy 设置为当前 _observer 截屏2021-12-11 下午1.31.50.png

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

截屏2021-12-11 下午1.34.34.png

  • 去订阅 RxList 中的 Stream 截屏2021-12-11 下午1.36.12.png

  • 正式监听 截屏2021-12-11 下午1.37.42.png

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

截屏2021-12-11 下午1.39.56.png

  • 最终触发 _ObxState 中的 _updateTree

截屏2021-12-11 下午1.41.05.png

  • 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.png

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 这个词应该是慎用的。

截屏2021-12-11 下午4.05.28.png

  • 过于夸张的描述

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

结语

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

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

  1. README

  2. 依赖管理

  3. 状态管理

  4. 路由管理

Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群:181398081

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