一、GetX框架
1、状态管理
- Obx是配合Rx响应式变量使用、GetBuilder是配合update使用。这完全是俩套定点刷新控件的方案。
区别:前者响应式,变量变化,Obx自动刷新;后者需要使用update手动调用刷新
- 每一个响应式变量,都需要生成对应的GetStream,占用资源大于基本数据类型,会对内存造成一定压力
- GetBuilder内部实际上是对StatefulWidget的封装,所以占用资源极小
- 推荐使用GetBuilder的方式进行刷新,结合update([id])方式实现局部刷新;避免大范围使用Obx的响应式方式;
2、生命周期
有了 GetxController 的生命周期后,我们就可以完全替换掉 StatefulWidget 了。
- onInit 替换 initState,比如初始化Controller,读取Get.arguments参数等
- onReady 处理异步动作,如调用接口
- onClose 替换 dispose,比如关闭流
@override
void onInit() {
super.onInit();
// 初始化Controller
stateBodyController = JStateBodyController();
refreshController = RefreshController(initialRefresh: false);
_stream = JEventBus.instance.on<FilterItemClickedEvent>().listen((event) {
update();
});
}
@override
void onReady() {
super.onReady();
/// 获取数据
fetchData();
}
@override
void onClose() {
_stream?.cancel();
_stream = null;
stateBodyController!.dispose();
refreshController!.dispose();
super.onClose();
}
3、控制器注入
1)静态路由绑定
方式1:GetPage中使用使用BindingsBuilder
GetPage(
name: "/$publishSuccess",
page: () => PublishSuccessPage(),
binding: BindingsBuilder(() => Get.lazyPut<PublishSuccessController>(
() => PublishSuccessController())))
方式2:从Bindings派生类,而后在GetPage中使用
class UpdateAddressBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<UpdateAddressController>(
() => UpdateAddressController(),
);
}
}
GetPage(
name: _Paths.updateAddress,
page: () => UpdateAddressView(),
binding: UpdateAddressBinding(),
)
对于通过静态路由绑定的Controller,推荐使用GetView。GetView只是对已注册的Controller有一个名为controller的getter的const Stateless的Widget,如果我们只有单个控制器作为依赖项,那我们就可以使用GetView,而不是使用StatelessWidget,并且避免了写Get.Find()
abstract class GetView<T> extends StatelessWidget {
const GetView({Key? key}) : super(key: key);
final String? tag = null;
T get controller => GetInstance().find<T>(tag: tag)!;
@override
Widget build(BuildContext context);
}
PS:Get.put与Get.lazyPut的区别在于,lazePut表示延迟初始化,在需要用到的时候才会初始化实例对象,即第一次 find 某一个类的时候才会进行初始化。
2)手动注入
- 在成员变量中,通过Get.put方法注入,不建议使用该方法,可以直接用GetView方式替代
- 在构造函数或Build函数中,通过Get.put方法注入,通常在Get.put带tag参数时,使用该方法
FilterItemGroupWidget(
{Key? key, required FilterItemGroupModel model, required this.configTag})
: _groupName = '${model.groupName}$configTag',
super(key: key) {
Get.put(FilterItemGroupController(model), tag: _groupName);
}
FilterWareCategoryWidget({
Key? key,
required this.selectedCategoryIds,
required this.manager,
required this.onClickCategory,
required this.onSubmit,
this.onReset,
}) : super(key: key) {
controller = Get.put<FilterWareCategoryController>(
FilterWareCategoryController(manager: manager));
controller.categoryModel.selectedCategoryIds = selectedCategoryIds;
}
PS1:对于未释放Controller,需要更新Controller数据时,需要在获取Controller后重新设置数据
PS2:使用PageView时,所有PageView页面控制器都被初始化? 如果在成员变量中调用Put,会发现所有的PageView页面控制器全被初始化了!并不是切换到某个页面时,对应页面的控制器才被初始化!若需要切换时才注入,将其移入到build方法中初始化。
3)GetBuilder时初始化
GetBuilder<CounterController>(
init: CounterController(),
builder: (_) => Text(
'${CounterController.to.counter}',
style: TextStyle(
color: Colors.blue,
fontSize: 24.0,
),
),
),
)
4、源码分析
1)控制器注入
- 主要的逻辑看来还是 GetInstance 中
S put<S>(
S dependency, {
String? tag,
bool permanent = false,
}) {
_insert(
isSingleton: true,
name: tag,
permanent: permanent,
builder: (() => dependency));
return find<S>(tag: tag);
}
/// Injects the Instance [S] builder into the `_singleton` HashMap.
void _insert<S>({
bool? isSingleton,
String? name,
bool permanent = false,
required InstanceBuilderCallback<S> builder,
bool fenix = false,
}) {
final key = _getKey(S, name);
_InstanceBuilderFactory<S>? dep;
if (_singl.containsKey(key)) {
final _dep = _singl[key];
if (_dep == null || !_dep.isDirty) {
return;
} else {
dep = _dep as _InstanceBuilderFactory<S>;
}
}
_singl[key] = _InstanceBuilderFactory<S>(
isSingleton: isSingleton,
builderFunc: builder,
permanent: permanent,
isInit: false,
fenix: fenix,
tag: name,
lateRemove: dep,
);
}
- 全局的数据都是存在 _singl 中,这是个 Map
static final Map<String, _InstanceBuilderFactory> _singl = {};
- key:对象的 runtimeType 或者类的 Type + tag
/// Generates the key based on [type] (and optionally a [name])
/// to register an Instance Builder in the hashmap.
String _getKey(Type type, String? name) {
return name == null ? type.toString() : type.toString() + name;
}
- value:_InstanceBuilderFactory 类,我们传入 dependedt 对象会存入这个类中
- _singl 这个 map 存值的时候,不是用的 put,而是用的 putIfAbsent
- 如果 map 中有 key 和传入 key 相同的数据,传入的数据将不会被存储
- 也就是说相同类实例的对象,传入并不会被覆盖,只会存储第一条数据,后续被放弃
- 最后使用 find 方法,返回传入的实例
2)find函数代码
S? _initDependencies<S>({String? name}) {
final key = _getKey(S, name);
final isInit = _singl[key]!.isInit;
S? i;
if (!isInit) {
final isSingleton = _singl[key]?.isSingleton ?? false;
if (isSingleton) {
_singl[key]!.isInit = true;
}
i = _startController<S>(tag: name);
if (isSingleton) {
if (Get.smartManagement != SmartManagement.onlyBuilder) {
RouterReportManager.instance
.reportDependencyLinkedToRoute(_getKey(S, name));
}
}
}
return i;
}
/// Holds a reference to `Get.reference` when the Instance was
/// created to manage the memory.
final Map<T?, List<String>> _routesKey = {};
/// Stores the onClose() references of instances created with `Get.create()`
/// using the `Get.reference`.
/// Experimental feature to keep the lifecycle and memory management with
/// non-singleton instances.
final Map<T?, HashSet<Function>> _routesByCreate = {};
static RouterReportManager? _instance;
RouterReportManager._();
static RouterReportManager get instance =>
_instance ??= RouterReportManager._();
void printInstanceStack() {
Get.log(_routesKey.toString());
}
T? _current;
// ignore: use_setters_to_change_properties
void reportCurrentRoute(T newRoute) {
_current = newRoute;
}
/// Links a Class instance [S] (or [tag]) to the current route.
/// Requires usage of `GetMaterialApp`.
void reportDependencyLinkedToRoute(String depedencyKey) {
if (_current == null) return;
if (_routesKey.containsKey(_current)) {
_routesKey[_current!]!.add(depedencyKey);
} else {
_routesKey[_current] = <String>[depedencyKey];
}
}
使用Get路由时,路由删除时调用didRemove
@override
void didRemove(Route route, Route? previousRoute) {
super.didRemove(route, previousRoute);
final routeName = _extractRouteName(route);
final currentRoute = _RouteData.ofRoute(route);
Get.log("REMOVING ROUTE $routeName");
_routeSend?.update((value) {
value.route = previousRoute;
value.isBack = false;
value.removed = routeName ?? '';
value.previous = routeName ?? '';
// value.isSnackbar = currentRoute.isSnackbar ? false : value.isSnackbar;
value.isBottomSheet =
currentRoute.isBottomSheet ? false : value.isBottomSheet;
value.isDialog = currentRoute.isDialog ? false : value.isDialog;
});
if (route is GetPageRoute) {
RouterReportManager.instance.reportRouteWillDispose(route);
}
routing?.call(_routeSend);
}
reportRouteWillDispose函数
static void reportDependencyLinkedToRoute(String depedencyKey) {
if (_current == null) return;
if (_routesKey.containsKey(_current)) {
_routesKey[_current!]!.add(depedencyKey);
} else {
_routesKey[_current] = <String>[depedencyKey];
}
}
调用堆栈
释放时堆栈
3)GetBuilder类代码
const GetBuilder({
Key? key,
this.init,
this.global = true,
required this.builder,
this.autoRemove = true,
this.assignId = false,
this.initState,
this.filter,
this.tag,
this.dispose,
this.id,
this.didChangeDependencies,
this.didUpdateWidget,
})
BindElement的dispose函数
void dispose() {
widget.dispose?.call(this);
if (_isCreator! || widget.assignId) {
if (widget.autoRemove && Get.isRegistered<T>(tag: widget.tag)) {
Get.delete<T>(tag: widget.tag);
}
}
for (final disposer in disposers) {
disposer();
}
disposers.clear();
_remove?.call();
_controller = null;
_isCreator = null;
_remove = null;
_filter = null;
_needStart = null;
_controllerBuilder = null;
_controller = null;
}
若设置autoRemove为true,assignId为true,在GetBuilder调用dispose时,controller将被释放
4)Get.bottomSheet弹出,Controller无法释放?
class SetSinglePriceStockPage extends StatelessWidget {
final SetPriceStockModel model;
///是否设置的是价格
final bool isSetPrice;
SetSinglePriceStockPage(
{Key? key, required this.model, required this.isSetPrice})
: super(key: key) {
Get.put(SetSinglePriceStockController(model));
JUi.setPreferredOrientations();
}
@override
Widget build(BuildContext context) {
return KeyboardMediaQuery(
child: JBindWidget(
bind: Get.find<SetSinglePriceStockController>(),
child: GetBuilder<SetSinglePriceStockController>(builder: (controller) {
return Container(
constraints: BoxConstraints(minHeight: 194.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
JUi.buildNavTitle(title: "修改价格/库存"),
Padding(
padding: EdgeInsets.all(12.w),
child: _buildPriceStock(controller),
),
SizedBox(
height: 12.w,
),
_buildBottom(controller)
],
),
);
}),
));
}
Widget _buildPriceStock(SetSinglePriceStockController controller) {
return PriceStockValueWidget(
key: ValueKey<String>(controller.skuPropValueModel.skuName),
isShowTitle: false,
model: controller.skuPropValueModel,
isPartition: controller.model.isPartition,
autoFocusPrice: isSetPrice,
autoFocusStock: !isSetPrice,
);
}
Widget _buildBottom(SetSinglePriceStockController controller) {
return JBottomSafeView(
child: Row(children: [
Expanded(
child: JButton(
type: ButtonType.normal,
size: ButtonSize.large,
onPressed: () {
Get.back();
},
child: const Text("取消"),
)),
SizedBox(width: 8.w),
Expanded(
child: JButton(
type: ButtonType.primary,
size: ButtonSize.large,
onPressed: () {
controller.updatePriceStock();
},
child: const Text("确定"),
))
]));
}
}
界面弹出后,关闭
[GETX] "SetSinglePriceStockController" onDelete() called
[GETX] "SetSinglePriceStockController" deleted from memory
[GETX] "PriceStockValueController" onDelete() called
[GETX] "PriceStockValueController" deleted from memory
去掉JBindWidget后,输入日志显示PriceStockValueController删除了,但SetSinglePriceStockController未删除,这是为什么呢?
弹出SetSinglePriceStockPage的方法
bool? result = await JPopup.showBottomSheet(
SetSinglePriceStockPage(model: model, isSetPrice: isSetPrice));
if (result != null && result) {
refreshData();
}
return Get.bottomSheet(
itemView,
backgroundColor: backgroundColor ?? JColors.white,
barrierColor: barrierColor ?? const Color.fromRGBO(0, 0, 0, 0.65),
ignoreSafeArea: ignoreSafeArea ?? true,
//这个设置true,当键盘弹出后,可以正常显示
isScrollControlled: isScrollControlled,
return Navigator.of(overlayContext!, rootNavigator: useRootNavigator)
.push(GetModalBottomSheetRoute<T>(
builder: (_) => bottomsheet,
isPersistent: persistent,
// theme: Theme.of(key.currentContext, shadowThemeOnly: true),
theme: Theme.of(key.currentContext!),
isScrollControlled: isScrollControlled,
class GetModalBottomSheetRoute<T> extends PopupRoute<T> {
GetModalBottomSheetRoute({
this.builder,
this.theme,
this.barrierLabel,
this.backgroundColor,
this.isPersistent,
this.elevation,
this.shape,
this.removeTop = true,
this.clipBehavior,
this.modalBarrierColor,
this.isDismissible = true,
this.enableDrag = true,
required this.isScrollControlled,
RouteSettings? settings,
this.enterBottomSheetDuration = const Duration(milliseconds: 250),
this.exitBottomSheetDuration = const Duration(milliseconds: 200),
}) : super(settings: settings) {
RouterReportManager.reportCurrentRoute(this);
}
修改为使用GetBuilder初始化后,弹出窗口的Controller正常释放
@override
Widget build(BuildContext context) {
return KeyboardMediaQuery(
child: GetBuilder<SetSinglePriceStockController>(
init: SetSinglePriceStockController(model),
builder: (controller) {
return Container(
constraints: BoxConstraints(minHeight: 194.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
JUi.buildNavTitle(title: "修改价格/库存"),
Padding(
padding: EdgeInsets.all(12.w),
child: _buildPriceStock(controller),
),
SizedBox(
height: 12.w,
),
_buildBottom(controller)
],
),
);
}),
);
}