前言
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());
// 如果你想这个实例永远存在,不被删除,可以把 permanent 设置为 true
Get.put(FFController(), permanent: true);
// 如果你的场景中,会存在多个相同的 FFController 实例,你可以用 tag 来进行区分
Get.put(FFController(), tag: 'unique key');
// 使用的时候才创建类
Get.lazyPut(() => FFController());
// 注册一个异步实例
Get.putAsync(() async => FFController());
获取
// 普通方式
FFController controller = Get.find();
// 如果你的场景中,会存在多个相同的 FFController 实例,你可以用 tag 来进行区分
FFController controller = Get.find(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() => find();
/// Holds references to every registered Instance when using
/// `Get.put()`
static final Map _singl = {};
/// Holds a reference to every registered callback when using
/// `Get.lazyPut()`
// static final Map _factory = {};
}
状态管理
[图片上传失败...(image-1da0ea-1639806641182)]
在讲这一部分的之前,再次重申下,框架再怎么骚操作,最终都会回归到
setState(() {});
。
Obx
这是 GetX
当中最大的一个魔法,我们先看看它是怎么用的。
obs
我们先在 FFController
当中增加一个
数组变量,obs
是一个扩展方法,它将返回 RxList
,至于什么是 RxList
,我们这里暂时不深入,先看看是怎么使用的。
class FFController extends GetxController {
RxList list = [].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());
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 operator +(Iterable 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 subject = GetStream();
final _subscriptions = >{};
void refresh() {
subject.add(value);
}
我们来看看 Obx
里面藏着什么。
class Obx extends ObxWidget {
final WidgetCallback builder;
const Obx(this.builder);
@override
Widget build() => builder();
}
而 Obx
继承于 ObxWidget
。ObxWidget
是一个 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.has('builder', build));
}
@override
_ObxState createState() => _ObxState();
@protected
Widget build();
}
class _ObxState extends State {
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(RxNotifier observer, ValueGetter 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);
,就将神奇的 RxList
和 Obx
关联起来了。
@override
E operator [](int index) {
return value[index];
}
@override
int get length => value.length;
@override
@protected
List 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 list = [];
void add(int i) {
list.add(i);
update();
}
}
class RxListDemo extends StatelessWidget {
RxListDemo({Key? key}) : super(key: key);
FFController controller = Get.put(FFController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: GetBuilder(
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));
},
),
);
}
}
主要核心代码不多,原理简单讲下,利用 GetInstance
将 FFController
做监听,等 FFController
update
的时候刷新 GetBuilder
。在 dispose
的时候跟进条件释放 FFController
。
class GetBuilderState extends State>
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(tag: widget.tag);
if (widget.global) {
if (isRegistered) {
if (GetInstance().isPrepared(tag: widget.tag)) {
_isCreator = true;
} else {
_isCreator = false;
}
controller = GetInstance().find(tag: widget.tag);
} else {
controller = widget.init;
_isCreator = true;
GetInstance().put(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(tag: widget.tag)) {
GetInstance().delete(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);
// 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(
// 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 navigatorKey =
GlobalKey(debugLabel: 'navigate');
@override
_AppState createState() => _AppState();
}
class _AppState extends State {
@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
,而 Page
继承于 RouteSettings
. 它是对一个页面的描述。通过 GetPage
组装成 GetPageRoute
。
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => MyHomePage(),
),
GetPage(
name: '/profile/',
page: () => MyProfile(),
),
],
)
GetPageRoute
MaterialPageRoute
和 CupertinoPageRoute
大家都应该很熟悉,GetPageRoute
和它们是一个东西。
class GetPageRoute extends PageRoute
with GetPageRouteTransitionMixin, PageRouteReportMixin {
}
不同的是它还有其他任务,它会在 install
(你可以简单理解为 push
) 和 dispose
(你可以简单理解为 pop
) 的时候去通知 RouterReportManager
。
mixin PageRouteReportMixin on Route {
@override
void install() {
super.install();
RouterReportManager.reportCurrentRoute(this);
}
@override
void dispose() {
super.dispose();
RouterReportManager.reportRouteDispose(this);
}
}
而 RouterReportManager
的任务之一就是去管理我们在当前页面注册的各种实例,下面为部分重要的代码。
class RouterReportManager {
static final Map> _routesKey = {};
static final Map> _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] = [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
获取参数,弱类型让人很不舒服。
为此我特意写增加了 FFRoute
和 GetX
结合的例子。(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?,
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)]
-
过于夸张的描述
一些描述过于浮夸,这也是导致
GetX
被Flutter Team
取消掉Flutter Favorite
的原因之一。
结语
GetX
是一个现象级的三方库,如何使用它,完全根据你自身的情况。建议新手不要上来就使用三方框架,它们会阻碍你对 Flutter
原理的理解。实际上,技术往往没有什么错误,只是使用的人不一样而已。
最后放上 GetX
官方中文文档:
README
依赖管理
状态管理
路由管理
爱 Flutter
,爱糖果
,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果[图片上传失败...(image-a3b79c-1639806641182)]QQ群:181398081
最最后放上 Flutter Candies 全家桶,真香。
[图片上传失败...(image-fa704b-1639806641182)]