Flutter getx组件使用和原理分析

546 阅读6分钟

9ac5bad1-03cc-4e0a-8ba6-e1fe09720296.jpeg 之前了解过Provider和Bloc的大概工作原理,对于要依赖context总觉得有点被束缚的地方,而Getx后来者居上,众说纷纭,褒贬不一,这玩意到底是个什么,特此探究一番记录。

一、先说使用

说完该使用我们再解释为什么这样用可以,以及Getx是如何实现刷新的,我们从view展示层,和逻辑logic层两方面来说。 先上一个完整demo如下: controller的代码如下

class GetDempController extends GetxController {
  var studend = Student();

  void creaseEnglishScore() {
    studend.englishScore++;
    update();
  }

  void creaseChineseScore() {
    studend.chineseScore++;
    update();
  }

  void creaseMatchScoreObs() {
    studend.mathScoreObs++;
  }

  void creaseMatchScore() {
    studend.mathScore++;
  }

  void creaseTeacherAge() {
    studend.teacher.creaseTeacherAge();
    update(['teach_age']);
  }

  GetDempController();
}

class Student {
  int englishScore = 0;
  int chineseScore = 0;
  var mathScoreObs = 0.obs;
  int mathScore = 0;
  Teacher teacher = Teacher();
}

class Teacher {
  int age = 30;

  void creaseTeacherAge() {
    age++;
  }
}

view层demo的完整代码如下

class GetDemoFirstPage extends StatelessWidget {
  GetDemoFirstPage({super.key});
  final GetDempController controller = Get.put(GetDempController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('渲染方式'),
        centerTitle: true,
      ),
      body: Column(
        children: [
          GetBuilder<GetDempController>(
            builder: (controller) {
              return Text('语文== ${controller.studend.chineseScore}');
            },
          ),
          GetBuilder<GetDempController>(
            builder: (controller) {
              return Text('英语= ${controller.studend.englishScore}');
            },
          ),
          Obx(() {
            return Text('数学Obs==${controller.studend.mathScoreObs.value}');
          }),
          // Obx(() {
          //   return Text('数学${controller.studend.mathScore}');
          // }),
          GetBuilder<GetDempController>(
            id: 'teach_age',
            builder: (controller) {
              return Text('老师年龄= ${controller.studend.teacher.age}');
            },
          ),
          InkWell(
            onTap: () {
              controller.creaseChineseScore();
            },
            child: Container(
              height: 50,
              color: Colors.red,
              alignment: Alignment.center,
              child: const Text('语文成绩++'),
            ),
          ),
          InkWell(
            onTap: () {
              controller.creaseEnglishScore();
            },
            child: Container(
              height: 50,
              color: Colors.yellow,
              alignment: Alignment.center,
              child: const Text('英语成绩++'),
            ),
          ),
          InkWell(
            onTap: () {
              controller.creaseMatchScoreObs();
            },
            child: Container(
              height: 50,
              color: Colors.blue,
              alignment: Alignment.center,
              child: const Text('Obs数学成绩++'),
            ),
          ),
          InkWell(
            onTap: () {
              controller.creaseMatchScore();
            },
            child: Container(
              height: 50,
              color: Colors.blue,
              alignment: Alignment.center,
              child: const Text('Obs数学成绩++'),
            ),
          ),
          InkWell(
            onTap: () {
              controller.creaseTeacherAge();
            },
            child: Container(
              height: 50,
              color: Colors.cyan,
              alignment: Alignment.center,
              child: const Text('老师年龄++'),
            ),
          ),
          InkWell(
            onTap: () {
              Navigator.push(
                  context,
                  CupertinoPageRoute(
                    builder: (context) => const SecondPage(),
                  ));
            },
            child: Container(
              height: 50,
              color: Colors.blue,
              alignment: Alignment.center,
              child: const Text('推第二个页面'),
            ),
          )
        ],
      ),
    );
  }
}

view层在使用之前,要确保controller被put到Get全局里面去,controller继承GetxController,这个类承载逻辑层的业务。

class GetDemoFirstPage extends StatelessWidget {
  GetDemoFirstPage({super.key});
  final GetDempController controller = Get.put(GetDempController());

  @override
  Widget build(BuildContext context) {

刷新页面写法分两种

1. 通过Obx包裹

Obx(() {
            return Text('数学Obs==${controller.studend.mathScoreObs.value}');
          }),

要求需要被更新的值按照.obs这种模式来声明,如下:

var mathScoreObs = 0.obs;

如果Obx包裹的内容没有.obs的值更新,运行时会throw错误,等下原理篇中讲解为什么。

我比较推荐通过GetBuilder去实现,这个obx总觉得怪怪的,不符合编程习惯。

2. 是通过GetBuilder

GetBuilder<GetDempController>(
            builder: (controller) {
              return Text('语文== ${controller.studend.chineseScore}');
            },
          ),

GetBuilder的属性按照常规声明即可,每次值更新以后要手动调用update去更新。

二、原理篇

Getx的使用非常简洁,且不像bloc和Inherited依赖context,来来来,我们来看看这是如何实现的。

Obs如何刷新界面

obs到底做了什么?

我们点.obs发现Getx对我们的基础类型都写了extension,通过get obs转成了Rx类型

extension IntExtension on int {
  /// Returns a `RxInt` with [this] `int` as initial value.
  RxInt get obs => RxInt(this);
}

再往深层点击,发现所有的类型都继承Rx Rx的完整代码如下,内部并没有什么东西,核心代码都在继承的这个_RxImpl中

    class Rx<T> extends _RxImpl<T> {
  Rx(T initial) : super(initial);

  @override
  dynamic toJson() {
    try {
      return (value as dynamic)?.toJson();
    } on Exception catch (_) {
      throw '$T has not method [toJson]';
    }
  }
}

类关系图

我先画一个各类之前的大概关系图,如下,大伙脑子里大概有个概念。

image.png

GetStream

说这个核心代码之前先说一下GetStream类吧,GetStream的简化代码如下,

 class GetStream<T> {
  void Function()? onListen;
  void Function()? onPause;
  void Function()? onResume;
  FutureOr<void> Function()? onCancel;

  GetStream({this.onListen, this.onPause, this.onResume, this.onCancel});
  List<LightSubscription<T>>? _onData = <LightSubscription<T>>[];

  bool? _isBusy = false;

  FutureOr<bool?> removeSubscription(LightSubscription<T> subs) async {
    if (!_isBusy!) {
      return _onData!.remove(subs);
    } else {
      await Future.delayed(Duration.zero);
      return _onData?.remove(subs);
    }
  }

  FutureOr<void> addSubscription(LightSubscription<T> subs) async {
    if (!_isBusy!) {
      return _onData!.add(subs);
    } else {
      await Future.delayed(Duration.zero);
      return _onData!.add(subs);
    }
  }

  int? get length => _onData?.length;

  bool get hasListeners => _onData!.isNotEmpty;

  void _notifyData(T data) {
    _isBusy = true;
    for (final item in _onData!) {
      if (!item.isPaused) {
        item._data?.call(data);
      }
    }
    _isBusy = false;
  }

....
    
  T? _value;

  T? get value => _value;

  void add(T event) {
    assert(!isClosed, 'You cannot add event to closed Stream');
    _value = event;
    _notifyData(event);
  }

 ....

  LightSubscription<T> listen(void Function(T event) onData,
      {Function? onError, void Function()? onDone, bool? cancelOnError}) {
    final subs = LightSubscription<T>(
      removeSubscription,
      onPause: onPause,
      onResume: onResume,
      onCancel: onCancel,
    )
      ..onData(onData)
      ..onError(onError)
      ..onDone(onDone)
      ..cancelOnError = cancelOnError;
    addSubscription(subs);
    onListen?.call();
    return subs;
  }

  Stream<T> get stream =>
      GetStreamTransformation(addSubscription, removeSubscription);
}

核心就是这_onData数组,数组是一个subscription订阅列表,通过listen往数组里面加订阅者数据,add方法调用_notifyData(T data),去遍历_onData,并去执行其中的_data方法。

    void _notifyData(T data) {
    _isBusy = true;
    for (final item in _onData!) {
      if (!item.isPaused) {
        item._data?.call(data);
      }
    }
    _isBusy = false;
  }
刷新核心逻辑

说完这个GetStream,咱们接着说这个核心代码,根据上图,可以看出主要的实现代码都在RxObjectMixin和NotifyManager中。RxObjectMixin和NotifyManager中的代码如下,

    mixin NotifyManager<T> {
  GetStream<T> subject = GetStream<T>();
  final _subscriptions = <GetStream, List<StreamSubscription>>{};

  bool get canUpdate => _subscriptions.isNotEmpty;

  /// This is an internal method.
  /// Subscribe to changes on the inner stream.
  void addListener(GetStream<T> rxGetx) {
    if (!_subscriptions.containsKey(rxGetx)) {
      final subs = rxGetx.listen((data) {
        if (!subject.isClosed) subject.add(data);
      });
      final listSubscriptions =
          _subscriptions[rxGetx] ??= <StreamSubscription>[];
      listSubscriptions.add(subs);
    }
  }

  StreamSubscription<T> listen(
    void Function(T) onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) =>
      subject.listen(
        onData,
        onError: onError,
        onDone: onDone,
        cancelOnError: cancelOnError ?? false,
      );

  /// Closes the subscriptions for this Rx, releasing the resources.
  void close() {
    _subscriptions.forEach((getStream, subscriptions) {
      for (final subscription in subscriptions) {
        subscription.cancel();
      }
    });

    _subscriptions.clear();
    subject.close();
  }
}
    mixin RxObjectMixin<T> on NotifyManager<T> {
  late T _value;

  ...
    
  set value(T val) {
    if (subject.isClosed) return;
    sentToStream = false;
    if (_value == val && !firstRebuild) return;
    firstRebuild = false;
    _value = val;
    sentToStream = true;
    subject.add(_value);
  }

  /// Returns the current [value]
  T get value {
    //调一次value,加一个订阅
    RxInterface.proxy?.addListener(subject);
    return _value;
  }

  Stream<T> get stream => subject.stream;

  ...

在RxObjectMixin的核心是value的set和get方法,通过源码,我们发现,每次get方法的时候,就会往RxInterface.proxy(GetStream的实例)中添加订阅,从上面GetStream源码分析,就是往subject的_ondata列表添加方法,而每次set方法的时候,就会去执行所有的subject中的_ondata中的所有方法。这个RxInterface.proxy划重点,后面接受这个的作用。 由此其实我们基本就想到了这个设计思路,就是针对obs对象,get对象的时候,相当于对这个对象添加一个订阅,而set这个对象的时候,就是更新值,通知所有的订阅者,值更新了请刷新界面。

那么是如何通知界面的呢? 我们点到ObxWidget中看看,ObxWidget的源码如下

    abstract class ObxWidget extends StatefulWidget {
  const ObxWidget({Key? key}) : super(key: key);

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    //把build添加到调试的构建树中
    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);
}

可以看到每次实例化一个ObxWidget的时候,就会声明一个RxNotifier类型的_observer,Notifier的主要实现

    class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;

而在init中,给_observe添加监听,代码如下

    @override
  void initState() {
    super.initState();
    subs = _observer.listen(_updateTree, cancelOnError: false);
  }

  void _updateTree(_) {
    if (mounted) {
      setState(() {});
    }
  }

我们来看看listen里面做了什么,调用subject的listen,也就是_obsever的subject(GetStream类型)的ondata列表里有widget的setState回调。

     StreamSubscription<T> listen(
    void Function(T) onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) =>
      subject.listen(
        onData,
        onError: onError,
        onDone: onDone,
        cancelOnError: cancelOnError ?? false,
      );

那这个回调什么时候触发呢,如果有印象的话,会记得我们前面说过一个触发回调的方法,RxObjectMixin中的set方法,会触发回调,那个Rx的中的subject和widget的subject如果关联呢,重点来了啊 看widget的build方法如下

    @override
  Widget build(BuildContext context) =>
      RxInterface.notifyChildren(_observer, widget.build);

我们点进去看看

     /// Avoids an unsafe usage of the `proxy`
  static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
    final oldObserver = RxInterface.proxy;
    RxInterface.proxy = observer;
    final result = builder();
    if (!observer.canUpdate) {
      RxInterface.proxy = oldObserver;
      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 = oldObserver;
    return result;
  }
}

这个方法中,build的时候先把当前需要build的这个widget的observer传给RxInterface.proxy,这是一个静态方法,然后调用了final result = builder();,在build的过程中,必然会调用Rx类型的get方法,我们再回头看看前面RxObjectMixin中的get方法,

    T get value {
    RxInterface.proxy?.addListener(subject);
    return _value;
  }

看到这里我的内心

image.png

这就连上了,get里RxInterface.proxy目前是当前widget的observer,addListener在NotifyManager里面实现

    void addListener(GetStream<T> rxGetx) {
    if (!_subscriptions.containsKey(rxGetx)) {
      final subs = rxGetx.listen((data) {
        if (!subject.isClosed) subject.add(data);
      });
      final listSubscriptions =
          _subscriptions[rxGetx] ??= <StreamSubscription>[];
      listSubscriptions.add(subs);
    }
  }

这个如下rxGetx如命名,就是Rx对象中的subject(GetStream类型的对象),Rx对象添加订阅,也就是rx的ondata列表中添加一个回调,这个回调就是widget的setState,前面我们说过,set的时候会遍历执行ondata列表,也就是set的时候执行widget的setSate。 一切都对上了。 正如前面我们开头说的,Obx包裹的内容没有.obs的值更新,运行时会throw错误,因为内部没有Rx类型的get,observer.canUpdate必然为空,因此会走到Throw中去。

    // Obx(() {
          //   return Text('数学${controller.studend.mathScore}');
          // }),

我们画一个图总结一下

image.png

GetBuilder是如何刷新界面的

相较于obx的刷新逻辑,GetBuilder的刷新逻辑相比起来更简单一点,讲一下主要的逻辑,我先上图为敬

image.png

    @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 {
      //调用init方法去初始化
      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,
          );
  }

GetBuilder实际上一个statefullWidget,在initState中首先会确保controller有值,然后调用_subscribeToController(),往controller的updaters(with过来的属性)数组里添加回调,当update调用的时候,会根据id去执行这些刷新页面的回调

三、controller销毁问题

写demo的过程中遇到一个问题,我没有用getx自带的路由,因为项目的路由已经比较成熟,改的话动的太多,影响太大,我发现我再Widget中put的controller并没有销毁,翻了很多资料,有建议修改assignId属性的

    class Lala extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return GetBuilder<LalaController>(builder: (logic) {
      return Text(
        '点击了 ${logic.count} 次',
        style: TextStyle(fontSize: 30.0),
      );
    },assignId: true,);
  }
}
                        
原文链接:https://blog.csdn.net/qq_26439323/article/details/129484608

也有建议把controller的put直接放到init中去的,

image.png

还有说要在dispose中手动调用delete的,看了半天都觉得不满意,不够优雅,直到看到 @法的空间 这位大佬的一个思路,既然controller无法释放是因为getx的路由感知不到widget的创建和释放,那就让他感知不就行了嘛,代码如下。

ceeb653ely1gee86ffyvdg207c07ctzo.gif

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage,
      ///此处配置下!
      navigatorObservers: [GetXRouterObserver()],
    );
  }
}

///自定义这个关键类!!!!!!
class GetXRouterObserver extends NavigatorObserver {
  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    RouterReportManager.reportCurrentRoute(route);
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) async {
    RouterReportManager.reportRouteDispose(route);
  }
}