GetX框架(非原创)

509 阅读4分钟

一、GetX框架

1、状态管理
  • Obx是配合Rx响应式变量使用、GetBuilder是配合update使用。这完全是俩套定点刷新控件的方案。

区别:前者响应式,变量变化,Obx自动刷新;后者需要使用update手动调用刷新

  • 每一个响应式变量,都需要生成对应的GetStream,占用资源大于基本数据类型,会对内存造成一定压力
  • GetBuilder内部实际上是对StatefulWidget的封装,所以占用资源极小
  • 推荐使用GetBuilder的方式进行刷新,结合update([id])方式实现局部刷新;避免大范围使用Obx的响应式方式;
2、生命周期

image.png

有了 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];
    }
  }

调用堆栈

image.png

释放时堆栈

image.png

image.png

image.png

3)GetBuilder类代码

image.png

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)
                ],
              ),
            );
          }),
    );
  }