前言
get | Flutter Package (flutter-io.cn) 一直是 Flutter 中帶有爭(zhēng)議的一個(gè)三方庫(kù)。正是因?yàn)橛袪?zhēng)議,所以我們應(yīng)該有自己的判斷,無(wú)需站隊(duì)。
一方面
它是 pub.dev 中點(diǎn)贊第一的庫(kù)
[圖片上傳失敗...(image-d10e8-1639806641181)]
Github Star 數(shù)量超過(guò) 5500
[圖片上傳失敗...(image-332ca1-1639806641182)]
擁有 140+ 的貢獻(xiàn)者
[圖片上傳失敗...(image-82d49d-1639806641182)]
1400+ 的 Issue
[圖片上傳失敗...(image-e28aac-1639806641182)]
這些都在說(shuō)明,這是一個(gè)熱度很高的三方組件庫(kù)。
另一方面
它也是開(kāi)發(fā)者吐槽的對(duì)象。
[圖片上傳失敗...(image-a3b865-1639806641182)]
[圖片上傳失敗...(image-c5fa6d-1639806641182)]
[圖片上傳失敗...(image-d52af1-1639806641182)]
[圖片上傳失敗...(image-677c48-1639806641182)]
可以看到,槽點(diǎn)還是滿滿的,我們暫時(shí)按下不表,先了解下什么是 GetX。
正題
GetX 是 Flutter 上的一個(gè)輕量且強(qiáng)大的解決方案:高性能的狀態(tài)管理、智能的依賴注入和便捷的路由管理 - 來(lái)自官方的描述。
官方文檔介紹的三大功能也是如此。
[圖片上傳失敗...(image-1727d1-1639806641182)]
我們下載一下 GetX 項(xiàng)目,打開(kāi)看看結(jié)構(gòu)是什么樣子的。
- 從文件夾上面可以大概看出來(lái)每個(gè)部分負(fù)責(zé)的功能.
get_connect: 網(wǎng)絡(luò)相關(guān)
get_instance: 注入相關(guān)
get_navigation: 路由相關(guān)
get_rx: 魔法相關(guān)(狗頭)
get_state_manager: 狀態(tài)相關(guān)
[圖片上傳失敗...(image-801795-1639806641182)]
- 不得不說(shuō),支持多個(gè)國(guó)家的文檔,這是很贊的事情。當(dāng)然,這是對(duì)于那些會(huì)看文檔的人來(lái)說(shuō)。
[圖片上傳失敗...(image-c176bc-1639806641182)]
接下來(lái)我將從源碼的角度,分析一下 GetX 的三大功能。
依賴管理
把 依賴管理 提到到前面來(lái)講,因?yàn)槠渌?個(gè)功能或多或少都基于它。
定義類
大部分情況下,這個(gè)類需要去繼承 GetxController,以便于整個(gè)系統(tǒng)自動(dòng)為它做
dispose 的操作(這部分會(huì)在路由管理中講)。
class FFController extends GetxController {}
注冊(cè)
// 普通方式
Get.put<FFController>(FFController());
// 如果你想這個(gè)實(shí)例永遠(yuǎn)存在,不被刪除,可以把 permanent 設(shè)置為 true
Get.put<FFController>(FFController(), permanent: true);
// 如果你的場(chǎng)景中,會(huì)存在多個(gè)相同的 FFController 實(shí)例,你可以用 tag 來(lái)進(jìn)行區(qū)分
Get.put<FFController>(FFController(), tag: 'unique key');
// 使用的時(shí)候才創(chuàng)建類
Get.lazyPut<FFController>(() => FFController());
// 注冊(cè)一個(gè)異步實(shí)例
Get.putAsync<FFController>(() async => FFController());
獲取
// 普通方式
FFController controller = Get.find<FFController>();
// 如果你的場(chǎng)景中,會(huì)存在多個(gè)相同的 FFController 實(shí)例,你可以用 tag 來(lái)進(jìn)行區(qū)分
FFController controller = Get.find<FFController>(tag: 'unique key');
原理
實(shí)際上,你跟代碼進(jìn)入 Get.put 或者 Get.find, 最終都指向 GetInstance。
GetInstance 其實(shí)就是一個(gè)單例(Dart 單線程真香?),它利用一個(gè) _singl Map 存儲(chǔ)著你注冊(cè)的對(duì)象/工廠方法,具體的過(guò)程不表。
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 = {};
}
狀態(tài)管理
[圖片上傳失敗...(image-1da0ea-1639806641182)]
在講這一部分的之前,再次重申下,框架再怎么騷操作,最終都會(huì)回歸到
setState(() {});。
Obx
這是 GetX 當(dāng)中最大的一個(gè)魔法,我們先看看它是怎么用的。
obs
我們先在 FFController 當(dāng)中增加一個(gè) <int>[] 數(shù)組變量,obs 是一個(gè)擴(kuò)展方法,它將返回 RxList<int>,至于什么是 RxList,我們這里暫時(shí)不深入,先看看是怎么使用的。
class FFController extends GetxController {
RxList<int> list = <int>[].obs;
}
Obx
使用 Obx 包含需要更新?tīng)顟B(tài)的部分,點(diǎn)擊 Icons.add 按鈕,你會(huì)發(fā)生整個(gè)列表發(fā)生改變。
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
對(duì)于 List 所以的方法和操作都做了 override,并且去調(diào)用 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 中是去執(zhí)行了 Stream.add 方法。那么 Stream 是誰(shuí)在消費(fèi)呢?
GetStream<T> subject = GetStream<T>();
final _subscriptions = <GetStream, List<StreamSubscription>>{};
void refresh() {
subject.add(value);
}
我們來(lái)看看 Obx 里面藏著什么。
class Obx extends ObxWidget {
final WidgetCallback builder;
const Obx(this.builder);
@override
Widget build() => builder();
}
而 Obx 繼承于 ObxWidget。ObxWidget 是一個(gè) StatefulWidget,在 _ObxState 初始化的時(shí)候 _observer 做了監(jiān)聽(tīng),當(dāng)它被通知的時(shí)候會(huì)觸發(fā)
_updateTree ,也就是我們常見(jiàn)的 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 傳遞進(jìn)去。其實(shí)我們可以看到這個(gè)方法只做了一件事情,在 builder 回調(diào)執(zhí)行之前,設(shè)置 RxInterface.proxy 為當(dāng)前 _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 方法中當(dāng) controller.list[index] 和 controller.list.length 被調(diào)用的時(shí)候。
return ListView.builder(
itemBuilder: (BuildContext b, int index) {
return Text('$index:${controller.list[index]}');
},
itemCount: controller.list.length,
);
會(huì)執(zhí)行 RxInterface.proxy?.addListener(subject); ,就將神奇的 RxList 和 Obx 關(guān)聯(lián)起來(lái)了。
@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;
}
接下來(lái)我們看看 debug 的堆棧信息,就能很清楚整個(gè)流程的運(yùn)作方式了。
- 創(chuàng)建監(jiān)聽(tīng)
[圖片上傳失敗...(image-336b63-1639806641182)]
將
RxInterface.proxy設(shè)置為當(dāng)前_observer
[圖片上傳失敗...(image-1ae901-1639806641182)]builder 回調(diào)中,即將觸發(fā)
RxList的神器魔法
[圖片上傳失敗...(image-296e6c-1639806641182)]
去訂閱
RxList中的Stream
[圖片上傳失敗...(image-876ddf-1639806641182)]正式監(jiān)聽(tīng)
[圖片上傳失敗...(image-b0cb7c-1639806641182)]當(dāng)我們對(duì)
RxList進(jìn)行改變,比如add的時(shí)候,觸發(fā)監(jiān)聽(tīng)
[圖片上傳失敗...(image-29c5ce-1639806641182)]
- 最終觸發(fā)
_ObxState中的_updateTree
[圖片上傳失敗...(image-c6ffca-1639806641182)]
-
Obxdispose的時(shí)候關(guān)閉流。
@override
void dispose() {
subs.cancel();
_observer.close();
super.dispose();
}
小結(jié)
.obs系列,包含對(duì)基礎(chǔ)的int,double,List等基礎(chǔ)結(jié)構(gòu)的封裝,并且包含了一個(gè)Stream來(lái)做通知。Obx通過(guò)對(duì)RxInterface.proxy的設(shè)置(該死的Dart單線程,真香! ),確保builder回調(diào)中的.obs只關(guān)聯(lián)當(dāng)前的RxInterface.proxy=》Obx,來(lái)確保當(dāng)前.obs只會(huì)觸發(fā)對(duì)應(yīng)Obx的刷新。你不需要?jiǎng)?chuàng)建
SreamController;你不需要為每個(gè)變量創(chuàng)建一個(gè)StreamBuilder;你不需要為每個(gè)變量創(chuàng)建ValueNotifier... 有一說(shuō)一,真香。
[圖片上傳失敗...(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));
},
),
);
}
}
主要核心代碼不多,原理簡(jiǎn)單講下,利用 GetInstance 將 FFController 做監(jiān)聽(tīng),等 FFController update 的時(shí)候刷新 GetBuilder。在 dispose 的時(shí)候跟進(jìn)條件釋放 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 中的對(duì)象的自動(dòng)釋放,又跟我們 GexX 的路由管理息息相關(guān)。
路由管理
Flutter 中的 context 是很重要的東西,很多 api 都是離不開(kāi)它的。你一定會(huì)有過(guò)這種想法,希望在沒(méi)有 context 的情況下使用路由,SnackBars , Dialogs , BottomSheets .
實(shí)際上,無(wú) context 路由的方法其實(shí)是很簡(jiǎn)單。
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(),
);
}
}
使用的時(shí)候你只需要
App.navigatorKey.currentState.pushNamed('/home');
而這一切 GexX 都為你封裝好了,你只需要將 MaterialApp 換成 GetMaterialApp 。
GetMaterialApp( // Before: MaterialApp(
home: MyHome(),
)
使用的時(shí)候你只需要這樣
Get.to(NextScreen());
Get.back();
Get.back(result: 'success');
Get.toNamed("/NextScreen");
Get.toNamed("/NextScreen", arguments: 'Get is the best');
// 獲取參數(shù)
print(Get.arguments);
當(dāng)然,GexX 的路由,遠(yuǎn)遠(yuǎn)不只你看到的這些,它更多的任務(wù)是串聯(lián)起了整個(gè) GexX 宇宙。
GetPage
GetPage 繼承于 Page<T> ,而 Page<T> 繼承于 RouteSettings. 它是對(duì)一個(gè)頁(yè)面的描述。通過(guò) GetPage 組裝成 GetPageRoute。
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => MyHomePage(),
),
GetPage(
name: '/profile/',
page: () => MyProfile(),
),
],
)
GetPageRoute
MaterialPageRoute 和 CupertinoPageRoute 大家都應(yīng)該很熟悉,GetPageRoute 和它們是一個(gè)東西。
class GetPageRoute<T> extends PageRoute<T>
with GetPageRouteTransitionMixin<T>, PageRouteReportMixin {
}
不同的是它還有其他任務(wù),它會(huì)在 install(你可以簡(jiǎn)單理解為 push ) 和 dispose(你可以簡(jiǎn)單理解為 pop ) 的時(shí)候去通知 RouterReportManager。
mixin PageRouteReportMixin<T> on Route<T> {
@override
void install() {
super.install();
RouterReportManager.reportCurrentRoute(this);
}
@override
void dispose() {
super.dispose();
RouterReportManager.reportRouteDispose(this);
}
}
而 RouterReportManager 的任務(wù)之一就是去管理我們?cè)诋?dāng)前頁(yè)面注冊(cè)的各種實(shí)例,下面為部分重要的代碼。
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新頁(yè)面觸發(fā)reportCurrentRoute,設(shè)置當(dāng)前_current。 - 當(dāng)在當(dāng)前頁(yè)面調(diào)用
Get.put的時(shí)候會(huì)調(diào)用到reportDependencyLinkedToRoute方法,保存起來(lái)。 -
pop頁(yè)面的時(shí)候觸發(fā)reportRouteDispose根據(jù)一些規(guī)則,釋放掉實(shí)例。
FFRoute
在實(shí)際使用中,下面 2 點(diǎn)是我不能習(xí)慣的。
- 手動(dòng)去設(shè)置
getPages集合 - 由于只能通過(guò)
Get.arguments獲取參數(shù),弱類型讓人很不舒服。
為此我特意寫(xiě)增加了 FFRoute 和 GetX 結(jié)合的例子。(FFRoute 是一個(gè)利用注解生成路由的工具)
ff_annotation_route/example_getx at master · fluttercandies/ff_annotation_route (github.com)
- 實(shí)際上,你只是需要在
onGenerateRoute回調(diào)中將FFRouteSettings轉(zhuǎn)為為對(duì)應(yīng)的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(),
);
},
);
}
}
- 使用的時(shí)候這樣寫(xiě)
Get.toNamed(Routes.itemPage.name,arguments: Routes.itemPage.d(index: index));
總結(jié)
這不是一篇介紹如何使用 GetX 的文章,只是從源碼的角度來(lái)簡(jiǎn)單地理解 GetX 三大功能的原理,僅此而已。
優(yōu)點(diǎn)
-
使用簡(jiǎn)單
如果你對(duì)
Flutter的原理有所理解,GetX絕對(duì)是大殺器,它能大大減少你編寫(xiě)代碼的時(shí)間。 -
功能豐富
除了狀態(tài)管理,依賴管理,路由管理三大功能,它還包含國(guó)際化,主題,網(wǎng)絡(luò)請(qǐng)求等,有一種全家桶的感覺(jué)。
缺點(diǎn)
-
使用簡(jiǎn)單
這是它的優(yōu)點(diǎn)也是它的缺點(diǎn)。它隱藏了
Flutter最基礎(chǔ)的原理。新手用起來(lái)可能很爽,但是如果遇到問(wèn)題很難去排查。很明顯的現(xiàn)象就是會(huì)有很多新手到群里問(wèn),GetX怎么不起作用了,時(shí)間長(zhǎng)了,確實(shí)很讓人沮喪。 -
功能豐富
太多的封裝,讓人不得不考慮到,如果這個(gè)庫(kù)停止更新了,會(huì)有多大的影響。盡管官方作出以下的承諾,但我想
always這個(gè)詞應(yīng)該是慎用的。
[圖片上傳失敗...(image-aca11a-1639806641182)]
-
過(guò)于夸張的描述
一些描述過(guò)于浮夸,這也是導(dǎo)致
GetX被Flutter Team取消掉Flutter Favorite的原因之一。
結(jié)語(yǔ)
GetX 是一個(gè)現(xiàn)象級(jí)的三方庫(kù),如何使用它,完全根據(jù)你自身的情況。建議新手不要上來(lái)就使用三方框架,它們會(huì)阻礙你對(duì) Flutter 原理的理解。實(shí)際上,技術(shù)往往沒(méi)有什么錯(cuò)誤,只是使用的人不一樣而已。
最后放上 GetX 官方中文文檔:
愛(ài) Flutter,愛(ài)糖果,歡迎加入Flutter Candies,一起生產(chǎn)可愛(ài)的Flutter小糖果[圖片上傳失敗...(image-a3b79c-1639806641182)]QQ群:181398081
最最后放上 Flutter Candies 全家桶,真香。
[圖片上傳失敗...(image-fa704b-1639806641182)]