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