preface

Prejudice in the heart is a mountain, no matter how hard you try, you can not move.

This is a sentence said by Shen Gongbao in the film Ne Zha, which is also a theme throughout the film. Perhaps this sentence has struck a chord with too many people: 35-year-old career crisis, Dachang card undergraduate degree, no house no car to get married difficult and so on, so this sentence is often mentioned.

At the same time, due to some of the comments made by the authors of GetX, there are also some stereotypes that have been attached to the GetX framework.

I’m not writing this article to justify GetX

  • I’m self-conscious that I’m not a big fan of either state framework, Provider and Bloc, and I’ve written about its use, dissecting principles, and related code generation plugins
  • In my mind, there is nothing mysterious about this kind of framework
  • Because it is familiar with the principle, it is relatively easy to get started, so switching the relevant framework does not have much time cost
  • So, I don’t need to be a defender

The overall design of GETX has a number of excellent ideas, which I hope to present to you; It may be helpful for you to design your own framework, as well as a record of your own thinking process.

Front knowledge

Before I get to GetX, there are a few things I need to know about the design philosophy behind Flutter, which has been a big part of its growth story

InheritedWidget

Have to say, this control is really a magic control, it seems to be a magic weapon

  • Treasure sword slay dragon, command the world, dare not from, rely on heaven not out, who and contend
  • Yitian sword, the sword hidden “Jiuyin Zhenjing”
  • Dragon Tu Dao, Dao contains 18 Palms of Descending Dragon and Legendary Letters of Wumu

What’s in this troll?

  • Dependent node, data transfer
  • Fixed point refresh mechanism

The data transfer

InheritedWidget is the name of a control we use all over again, and the essence is to use an inheritedElement and use an inheritedWidget to transfer data

Save the data

  • InheritedWidget stores data in an InheritedElement, which is a relatively simple operation
class TransferDataWidget extends InheritedWidget { TransferDataWidget({required Widget child}) : super(child: child); @override bool updateShouldNotify(InheritedWidget oldWidget) => false; @override InheritedElement createElement() => TransferDataElement(this); } class TransferDataElement extends InheritedElement { TransferDataElement(InheritedWidget widget) : super(widget); String value = 'transfer data '; String value =' transfer data '; }

Take the data

  • As long as it is a child of TransferDataWidget (a child of InheritedWidget), it can fetch data seamlessly via the child’s BuildContext (where Element is the implementation class of BuildContext)
var transferDataElement = context.getElementForInheritedWidgetOfExactType<TransferDataWidget>() as TransferDataElement? ; var msg = transferDataElement.value;

Can be found, we only need through Element getElementForInheritedWidgetOfExactType method, can get the parent node TransferDataElement instance (must inherit InheritedElement)

After getting the example, it is very easy to get the corresponding data

The principle of

  • You can see that we’ve got an instance of XXXInheritedElement, and we’ve got the stored value, So the key in getElementForInheritedWidgetOfExactType < T extends InheritedWidget > () this method

    • The code is very simple. It just evaluates from the map _inheritedWidgets where the generic T is the key
abstract class Element extends DiagnosticableTree implements BuildContext {

    Map<Type, InheritedElement>? _inheritedWidgets;

    @override
    InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
        assert(_debugCheckStateIsActiveForAncestorLookup());
        final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
        return ancestor;
    }
    
    ...
}
  • Then just figure out how _inheritedWidgets stores its value, and everything will be clear, right
abstract class ComponentElement extends Element { @mustCallSuper void mount(Element? parent, dynamic newSlot) { ... _updateInheritance(); } void _updateInheritance() { assert(_lifecycleState == _ElementLifecycle.active); _inheritedWidgets = _parent? ._inheritedWidgets; }... } abstract class ProxyElement extends ComponentElement { ... } class InheritedElement extends ProxyElement { InheritedElement(InheritedWidget widget) : super(widget); @override void _updateInheritance() { assert(_lifecycleState == _ElementLifecycle.active); final Map<Type, InheritedElement>? incomingWidgets = _parent? ._inheritedWidgets; if (incomingWidgets ! = null) _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets); else _inheritedWidgets = HashMap<Type, InheritedElement>(); _inheritedWidgets! [widget.runtimeType] = this; }}

The overall logic is relatively clear

  1. When an inheritedWidget node is encountered, the parent node’s _inheritedWidgets variable is given to the incomingWidgets temporary variable

    1. IncomingWidgets is empty: The _inheritedWidgets variable of the parent class Element, which instantiates a Map object
    2. IncomingWidgets is not empty: Use a deep copy of all data from the parent _inheritedWidgets to return a new Map instance containing all data from the parent _inheritedWidgets and assign it to the _inheritedWidgets variable of the parent Element class
  2. Assigns an instance of itself to the _inheritedWidgets variable of the parent class Element, with key being the runtimeType of its Widget

Why can the _inheritedWidgets variable of any Widget’s Element instance be taken directly to the parent node’s inheritedElement instance?

  • In Element, a parent node assigns a value to a child node: the entire data transfer chain is clear
abstract class Element extends DiagnosticableTree implements BuildContext {

    Map<Type, InheritedElement>? _inheritedWidgets;

    void _updateInheritance() {
        assert(_lifecycleState == _ElementLifecycle.active);
        _inheritedWidgets = _parent?._inheritedWidgets;
    }

    ...
}
  • graphic

The refresh mechanism

There are some interactions between inheritedElements and elements that actually come with a built-in refresh mechanism

  • InheritedElement storage child Element: _dependents. This variable is used to store the child Element that needs to be refreshed
class InheritedElement extends ProxyElement { InheritedElement(InheritedWidget widget) : super(widget); final Map<Element, Object? > _dependents = HashMap<Element, Object? > (); @protected void setDependencies(Element dependent, Object? value) { _dependents[dependent] = value; } @protected void updateDependencies(Element dependent, Object? aspect) { setDependencies(dependent, null); }}
  • Use an inheritedElement to refresh the child Element

    • NotifyClients is a method that takes all the elements from _dependents and passes them to NotifyDependent
    • In the NotifyDependency method, passing in the Element calls its own didChangeDependencies() method
    • Element’s didChangeDependencies() method calls markNeedsBuild() to refresh itself
class InheritedElement extends ProxyElement { InheritedElement(InheritedWidget widget) : super(widget); final Map<Element, Object? > _dependents = HashMap<Element, Object? > (); @protected void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) { dependent.didChangeDependencies(); } @override void notifyClients(InheritedWidget oldWidget) { for (final Element dependent in _dependents.keys) { ... notifyDependent(oldWidget, dependent); } } } abstract class Element extends DiagnosticableTree implements BuildContext { ... @mustCallSuper void didChangeDependencies() { assert(_lifecycleState == _ElementLifecycle.active); // otherwise markNeedsBuild is a no-op assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies')); markNeedsBuild(); }... }

How does the child node of an InheritedWidget use its Element

How about the _dependents variable added to an inheritedElement?

  • Element has a dependentOnInheritedElement method

    • Element dependent OnInheritedElement method passes in an InheritedElement instance ancestor
    • It passes in its Element instance by calling the updateDependencies method
    • We’re going to use an inheritedElement to add that Element to _dependents
abstract class Element extends DiagnosticableTree implements BuildContext { ... @override InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) { assert(ancestor ! = null); _dependencies ?? = HashSet<InheritedElement>(); _dependencies! .add(ancestor); ancestor.updateDependencies(this, aspect); return ancestor.widget; }... } class InheritedElement extends ProxyElement { InheritedElement(InheritedWidget widget) : super(widget); final Map<Element, Object? > _dependents = HashMap<Element, Object? > (); @protected void setDependencies(Element dependent, Object? value) { _dependents[dependent] = value; } @protected void updateDependencies(Element dependent, Object? aspect) { setDependencies(dependent, null); }}
  • DependOnInheritedElement is also simple to use

    • In general use in a Widget InheritedElement getElementForInheritedWidgetOfExactType access to the parent node
    • It is then passed into the DependOnInheritedElement method
/ / for var inheritedElement = context. GetElementForInheritedWidgetOfExactType < ChangeNotifierEasyP < T > > () as EasyPInheritedElement<T>? ; context.dependOnInheritedElement(inheritedElement);

The core principle of the Provider is to use an InheritedWidget refresh mechanism

To learn more about the principle of providers, refer to the following article

  • The other side of the Flutter Provider

graphic

  • Take a look at the graphical representation of the InheritedWidget refresh mechanism

Routing Tips

  • Routing Navigator is basically a static method to operate the route, and NavigatorState is the concrete logic of the implementation

You’ve probably had the trouble getting data in an InheritedWidget: Page A –>, B –>, C

If I store data in an InheritedWidget on page A and go to page B or page C, I can’t use the Context to retrieve an InheritedElement on page A

This side proves the Navigator route jump: page A jumps to page B, and page B is not A child node of page A

  • The Navigator is the parent node of all pages, and there is a horizontal relationship between pages

I have drawn the general structure here. If there is any deviation, please be sure to point it out. I will revise it as soon as possible

For an explanation of the Flutter’s routing principles, see this article: The Flutter’s routing principles

thinking

InheritedWidget takes a lot of convenience for us

  • We can use a Widget tree scope to get all inheritedElements we want to use.
  • InheritedElements and their various interactions also implement a very neat set of refresh mechanisms
  • Carry on some deep encapsulation, can even manage the resource release of many controls seamlessly

However, the way Element provides to get inheritedElements doesn’t ultimately fit well with the routing mechanism; This is also the inevitable thing of module design, perhaps the optimal solution of some module design, it is difficult to worry about some of the mechanism of other modules

InheritedWidget is a powerful tool that has helped us a lot in learning Flutter

  • But as the requirements get more complex, your skills get better
  • Dragon slaying sword, this divine soldier, perhaps gradually, is not very suitable for you
  • Sometimes, it even limits what you can do
  • In the mind of a blacksmith who makes a skillful soldier, the best weapon for a skillful soldier may always be the next

Most state management frameworks separate the interface layer from the logical layer, and the logical layer handles interface refresh; The logical layer can be handed over to InheritedWidget storage management; Explain, we also can store management certainly!

  • To manage the logical layer by yourself, you can get rid of the constraints of the Element tree, instead of being trapped in the parent and child nodes of the Element tree
  • In the routed jump page, you can easily get the previous page, the next page or the previous page logic layer.

This is one of the core ideas of GETX. It’s not a very new or sophisticated technology, but I think it’s a breakthrough in thinking that opens up a lot more possibilities

Dependency injection

instructions

Dependency injection can be implemented as follows (Wikipedia) :

  • Based on interfaces. Implement a specific interface for external containers to inject objects of dependent types.
  • Based on the SET method. Implementing a public set method with a specific property to let the external container call an object of the dependent type passed in.
  • Based on the constructor. Implements a constructor for a particular parameter, passing in an object of the dependent type when the object is created.
  • Annotations based. A Java-based annotation feature that allows an “@Autowired” annotation in front of a private variable allows the external container to pass in the corresponding object without explicitly defining the above three types of code. This scenario is equivalent to defining public set methods, but since there are no actual set methods, it does not expose interfaces that should not be exposed in order to implement dependency injection (because set methods only want container access to inject and do not want other objects that depend on such a method to access).

Of strongly coupled types, based on the constructor

class Test { String msg; Test(String msg) { this.msg = msg; }}

The set method

class Test { String? _msg; void setMsg(String msg) { this._msg = msg; }}

If, in Java, it is convenient to pass values directly to the constructor, and then more and more values are needed, the constructor needs to be added because of the strong coupling of many classes. (The Dart constructor’s optional parameter feature has no such problem.)

  • The GetXController injected by GetX is maintained by the GetX framework itself. What would the intermediate layer look like without GetX?

  • The middle tier of GetX is introduced to manage it

    • Look at the picture below and immediately think of the intermediary mode
    • This is also the idea of Inversion of Control (control of creating an object is given to a third party instead of being in your own hands)

Put

Let’s take a look at the operation of getX injection

  • Put to use
var controller = Get.put(XxxGetxController());
  • Look at what’s going on inside

    • Ah, all kinds of Sao operations
    • The main logic is in Inst, which is an extension of GetInterface
class _GetImpl extends GetInterface {}

final Get = _GetImpl();

extension Inst on GetInterface {
  S put<S>(S dependency,
          {String? tag,
          bool permanent = false,
          InstanceBuilderCallback<S>? builder}) =>
      GetInstance().put<S>(dependency, tag: tag, permanent: permanent);
}
  • The main logic seems to be in getInstance

    • You can look at the implementation of this singleton, I found a lot of source code are written in this way, very concise
    • The global data is stored in _singl, which is a Map

      • Key: the runtimeType of the object or the Type + tag of the class
      • Value: _instanceBuilderFactory class, into which the DepEnddt object will be stored
    • The _singl map stores values not with put, but with putIfAbsent

      • If the map has the same data as the incoming key, the incoming data will not be stored
      • In other words, objects of the same class instance will not be overwritten, only the first data will be stored, and then discarded
    • Finally, use the find method to return the instance passed in
class GetInstance {
  factory GetInstance() => _getInstance ??= GetInstance._();

  const GetInstance._();

  static GetInstance? _getInstance;

  static final Map<String, _InstanceBuilderFactory> _singl = {};

  S put<S>(
    S dependency, {
    String? tag,
    bool permanent = false,
    @deprecated InstanceBuilderCallback<S>? builder,
  }) {
    _insert(
        isSingleton: true,
        name: tag,
        permanent: permanent,
        builder: builder ?? (() => dependency));
    return find<S>(tag: tag);
  }

  void _insert<S>({
    bool? isSingleton,
    String? name,
    bool permanent = false,
    required InstanceBuilderCallback<S> builder,
    bool fenix = false,
  }) {
    final key = _getKey(S, name);
    _singl.putIfAbsent(
      key,
      () => _InstanceBuilderFactory<S>(
        isSingleton,
        builder,
        permanent,
        false,
        fenix,
        name,
      ),
    );
  }
    
  String _getKey(Type type, String? name) {
    return name == null ? type.toString() : type.toString() + name;
  }
    
  S find<S>({String? tag}) {
    final key = _getKey(S, tag);
    if (isRegistered<S>(tag: tag)) {
      if (_singl[key] == null) {
        if (tag == null) {
          throw 'Class "$S" is not registered';
        } else {
          throw 'Class "$S" with tag "$tag" is not registered';
        }
      }
      final i = _initDependencies<S>(name: tag);
      return i ?? _singl[key]!.getDependency() as S;
    } else {
      // ignore: lines_longer_than_80_chars
      throw '"$S" not found. You need to call "Get.put($S())" or "Get.lazyPut(()=>$S())"';
    }
  }
}

find

  • Find is a simple method that retrieves data from a map
S find<S>({String? tag}) => GetInstance().find<S>(tag: tag);
  • Let’s look at the logic

    • If _singl contains any data of this key, it will fetch it; if it does not, it will throw an exception
    • _singl[key]! .getDependency() as S, which is a key to the map
class GetInstance {
  factory GetInstance() => _getInstance ??= GetInstance._();

  const GetInstance._();

  static GetInstance? _getInstance;

  static final Map<String, _InstanceBuilderFactory> _singl = {};
    
  String _getKey(Type type, String? name) {
    return name == null ? type.toString() : type.toString() + name;
  }
    
  bool isRegistered<S>({String? tag}) => _singl.containsKey(_getKey(S, tag));
    
  S find<S>({String? tag}) {
    final key = _getKey(S, tag);
    if (isRegistered<S>(tag: tag)) {
      if (_singl[key] == null) {
        if (tag == null) {
          throw 'Class "$S" is not registered';
        } else {
          throw 'Class "$S" with tag "$tag" is not registered';
        }
      }
      final i = _initDependencies<S>(name: tag);
      return i ?? _singl[key]!.getDependency() as S;
    } else {
      // ignore: lines_longer_than_80_chars
      throw '"$S" not found. You need to call "Get.put($S())" or "Get.lazyPut(()=>$S())"';
    }
  }
}

GetBuilder refresh mechanism

use

For the sake of continuity of knowledge, it is simply written down here

  • Logic layer
class GetCounterEasyLogic extends GetxController { var count = 0; void increase() { ++count; update(); }}
  • interface
class GetCounterEasyPage extends StatelessWidget { final GetCounterEasyLogic logic = Get.put(GetCounterEasyLogic()); Override Widget build(buildContext Context) {return baseScaffold (appBar: appBar (title: const Text(' Counter-Simpler '))), body: Center(child: getCounterBuilder < getCounterLogic >(builder: (logic) {return Text(' ${logic.count} ', style: TextStyle (fontSize: 30.0),); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); }}

GetBuilder

One day, I was lying in bed thinking

  • OBX state management, GetXController instance recycling is placed in the route, in many scenarios, there are some limitations
  • Then it occurred to me that getBuilder uses a generic type, so you can get an instance of getXController, and getBuilder is StatefulWidget
  • In this way, you can use it to reclaim instances, which can solve the problem in many scenarios where the GetXController instance cannot be recycled (without using the GetX route).
  • I excitedly opened the GetX project, ready to raise PR, and then found that GetBuilder has written the operation of recycling instance in Dispose
  • Dubious!

Built-in recycling mechanism

  • Much of the code is condensed here, showing only the code for the collection mechanism
class GetBuilder<T extends GetxController> extends StatefulWidget { final GetControllerBuilder<T> builder; final bool global; final String? tag; final bool autoRemove; final T? init; const GetBuilder({ Key? key, this.init, this.global = true, required this.builder, this.autoRemove = true, this.initState, this.tag, }) : super(key: key); @override GetBuilderState<T> createState() => GetBuilderState<T>(); } class GetBuilderState<T extends GetxController> extends State<GetBuilder<T>> with GetStateUpdaterMixin { T? controller; bool? _isCreator = false; VoidCallback? _remove; Object? _filter; @override void initState() { super.initState(); widget.initState? .call(this); var isRegistered = GetInstance().isRegistered<T>(tag: widget.tag); if (widget.global) { if (isRegistered) { controller = GetInstance().find<T>(tag: widget.tag); } else { controller = widget.init; GetInstance().put<T>(controller! , tag: widget.tag); } } else { controller = widget.init; controller? .onStart(); } } @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 Widget build(BuildContext context) { return widget.builder(controller!) ; }}

The logic in the code is quite clear. InitState gets the instance and Dispose takes the instance

  1. Get the corresponding GetXController instance through the generic on GetBuilder

    • Non-existent: passed in instance using init
    • 2. To use directly; Init passed in an invalid instance
  2. AutoRemove controls whether or not the getXController instance is automatically reclaimed

    • Default to true: Auto collection is enabled by default
    • True: Enable automatic collection False: Enable automatic collection

Refresh the logic

  • Only the code related to the refresh logic is kept here, and the code that you don’t need to worry about is removed
mixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> { void getUpdate() { if (mounted) setState(() {}); } } class GetBuilder<T extends GetxController> extends StatefulWidget { final GetControllerBuilder<T> builder; final bool global; final T? init; final Object? id; const GetBuilder({ Key? key, this.init, this.id, this.global = true, required this.builder, }) : super(key: key); @override GetBuilderState<T> createState() => GetBuilderState<T>(); } class GetBuilderState<T extends GetxController> extends State<GetBuilder<T>> with GetStateUpdaterMixin { T? controller; @override void initState() { super.initState(); . if (widget.global) { if (isRegistered) { controller = GetInstance().find<T>(tag: widget.tag); } else { controller = widget.init; GetInstance().put<T>(controller! , tag: widget.tag); } } else { controller = widget.init; controller? .onStart(); } _subscribeToController(); } 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 didChangeDependencies() { super.didChangeDependencies(); widget.didChangeDependencies? .call(this); } @override void didUpdateWidget(GetBuilder oldWidget) { super.didUpdateWidget(oldWidget as GetBuilder<T>); if (oldWidget.id ! = widget.id) { _subscribeToController(); } widget.didUpdateWidget? .call(oldWidget, this); } @override Widget build(BuildContext context) { return widget.builder(controller!) ; }}

Key steps

  1. Get the injected instance of GetXController from the generic
  2. Add listening code

    • AddListener: Add a listening callback
    • AddListenerID: To add a listener callback, the ID must be set. The corresponding ID must also be written when updating the update
  3. Listener code: The core code is the getUpdate method, which is found in getStateUpdaterMixin

    • The getUpdate() logic is just setState(), which refreshes the current getBuilder

graphic

Update

  • The trigger logic is simple, just use Update

    • IDS: The getBuilder above corresponds to the getBuilder above. You can refresh the getBuilder corresponding to the setting ID
    • Condition: whether to refresh a condition, default to true (assuming an id greater than 3 is required to refresh: update([1, 2, 3, 4], index > 3))
abstract class GetxController extends DisposableInterface with ListNotifier { void update([List<Object>? ids, bool condition = true]) { if (! condition) { return; } if (ids == null) { refresh(); } else { for (final id in ids) { refreshGroup(id); }}}}
  • Take a look at the key method refresh(), in the ListNotifier class

    • As you can see, the generics in _updaters are a method
    • The listener added to GetBuilder is a method parameter, and inside the method body is setState().
    • His live! GetBuilder adds methods (method body is setState), and update traversal triggers all add methods
typedef GetStateUpdate = void Function(); class ListNotifier implements Listenable { List<GetStateUpdate? >? _updaters = <GetStateUpdate? > []; HashMap<Object? , List<GetStateUpdate>>? _updatersGroupIds = HashMap<Object? , List<GetStateUpdate>>(); @protected void refresh() { assert(_debugAssertNotDisposed()); _notifyUpdate(); } void _notifyUpdate() { for (var element in _updaters!) { element! (a); }}... }
  • If you add an ID parameter to the update, you will use the refreshGroup method. The logic is almost the same as that of refresh. The difference is that if there is an ID, you will execute it; if there is no ID, you will skip it

    • Iterate through all the IDS, and then execute the refreshGroup method
abstract class GetxController extends DisposableInterface with ListNotifier { void update([List<Object>? ids, bool condition = true]) { if (! condition) { return; } if (ids == null) { refresh(); } else { for (final id in ids) { refreshGroup(id); } } } } class ListNotifier implements Listenable { HashMap<Object? , List<GetStateUpdate>>? _updatersGroupIds = HashMap<Object? , List<GetStateUpdate>>(); void _notifyIdUpdate(Object id) { if (_updatersGroupIds! .containsKey(id)) { final listGroup = _updatersGroupIds! [id]! ; for (var item in listGroup) { item(); } } } @protected void refreshGroup(Object id) { assert(_debugAssertNotDisposed()); _notifyIdUpdate(id); }}

conclusion

  • Take a look at the GetBuilder refresh icon

OBX refresh mechanism

The refresh mechanism differs somewhat from the state management framework (Provider, Bloc) and GetBuilder above

  • Variables: basic types, entities, and data types such as lists are encapsulated in a set of RX types, which are quick to add OBS after the data

    • For example, rxString MSG = “test”.obs (var MSG = “test”.obs)
  • Update: The underlying type updates the data directly, and the entities need to be in the form of.update()
  • When using this type of variable, you usually add. Value. The author also gives a shortcut to add () to the variable.

    • I do not recommend the plus () form, because it is too unfriendly to the people who will maintain the project later

OBX refresh mechanism, the most interesting should be the variable change, wrapped in the OBX will automatically refresh! Note that only the Obx wrapped around the variable will refresh! Other OBX’s do not refresh.

How does this work?

  • In fact, it’s very simple to implement
  • However, if you have not been exposed to this idea, I am afraid that it is very difficult to scratch your head, but also to play so…

use

Let’s take a quick look at usage

  • logic
class GetCounterRxLogic extends GetxController { var count = 0.obs; Void increase() => ++count; }
  • view
class GetCounterRxPage extends StatelessWidget { final GetCounterRxLogic logic = Get.put(GetCounterRxLogic()); Override Widget build(buildContext Context) {return baseScaffold (appBar: appBar (title: const Text(' counter - response ')), body: Center(child: Obx(() {return Text(' Click ${logic.count.value} ', style: textStyle (fontSize: 30.0),); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); }}

Rx class variables

Take RxInt as an example here to look at its internal implementation

  • So let’s look at the extension obs after an integer, which is an extension class, 0. OBS is equal to rxInt (0).
extension IntExtension on int {
  /// Returns a `RxInt` with [this] `int` as initial value.
  RxInt get obs => RxInt(this);
}
  • Let’s take a look at rxInt: this makes it clear that when running with.value, it will automatically return a current instance and modify the value
class RxInt extends Rx<int> { RxInt(int initial) : super(initial); /// Addition operator. RxInt operator +(int other) { value = value + other; return this; } /// Subtraction operator. RxInt operator -(int other) { value = value - other; return this; }}
  • Let’s look at the parent class Rx is less than T>

    • An important class appears here: _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]'; }}}
  • The _rxImpl <T> class inherits RxNotifier<T> and with RxObjectMixin<T>

    • The content of this class is quite large, mainly RxNotifier

      and RxObjectMixin

      are quite large
    • A lot of code, first show the complete code; This will be simplified in the next illustration
abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> { _RxImpl(T initial) { _value = initial; } void addError(Object error, [StackTrace? stackTrace]) { subject.addError(error, stackTrace); } Stream<R> map<R>(R mapper(T? data)) => stream.map(mapper); void update(void fn(T? val)) { fn(_value); subject.add(_value); } void trigger(T v) { var firstRebuild = this.firstRebuild; value = v; if (! firstRebuild) { subject.add(v); } } } class RxNotifier<T> = RxInterface<T> with NotifyManager<T>; mixin NotifyManager<T> { GetStream<T> subject = GetStream<T>(); final _subscriptions = <GetStream, List<StreamSubscription>>{}; bool get canUpdate => _subscriptions.isNotEmpty; 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, ); 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; void refresh() { subject.add(value); } T call([T? v]) { if (v ! = null) { value = v; } return value; } bool firstRebuild = true; String get string => value.toString(); @override String toString() => value.toString(); dynamic toJson() => value; @override bool operator ==(dynamic o) { if (o is T) return value == o; if (o is RxObjectMixin<T>) return value == o.value; return false; } @override int get hashCode => _value.hashCode; set value(T val) { if (subject.isClosed) return; if (_value == val && ! firstRebuild) return; firstRebuild = false; _value = val; subject.add(_value); } T get value { if (RxInterface.proxy ! = null) { RxInterface.proxy! .addListener(subject); } return _value; } Stream<T? > get stream => subject.stream; void bindStream(Stream<T> stream) { final listSubscriptions = _subscriptions[subject] ?? = <StreamSubscription>[]; listSubscriptions.add(stream.listen((va) => value = va)); }}
  • Simplify _RxImpl<T>, this is too much, so let me simplify this and show you what I need to care about: there are a few important points here

    • RxInt is a data type with a built-in callback (getStream)
    • When the value variable of RxInt is changed (set value), it triggers subject.add(_value), and the internal logic is an automatic refresh operation
    • When you get value from an RxInt, there is an action to add a listener. This is important!
abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> { void update(void fn(T? val)) { fn(_value); subject.add(_value); } } class RxNotifier<T> = RxInterface<T> with NotifyManager<T>; mixin NotifyManager<T> { GetStream<T> subject = GetStream<T>(); final _subscriptions = <GetStream, List<StreamSubscription>>{}; bool get canUpdate => _subscriptions.isNotEmpty; 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); } } } mixin RxObjectMixin<T> on NotifyManager<T> { late T _value; void refresh() { subject.add(value); } set value(T val) { if (subject.isClosed) return; if (_value == val && ! firstRebuild) return; firstRebuild = false; _value = val; subject.add(_value); } T get value { if (RxInterface.proxy ! = null) { RxInterface.proxy! .addListener(subject); } return _value; }}
  • Why does getStream add refresh: Deletes a lot of code, but keeps the important code

    • When the add method is called, the _notifyData method is called
    • In the _notifyData method, the _onData list is traversed and its generic _data methods are executed depending on the condition
    • I’m going to guess that nine times out of ten, the method body in _data must have added setState() somewhere.
class GetStream<T> { GetStream({this.onListen, this.onPause, this.onResume, this.onCancel}); List<LightSubscription<T>>? _onData = <LightSubscription<T>>[]; FutureOr<void> addSubscription(LightSubscription<T> subs) async { if (! _isBusy!) { return _onData! .add(subs); } else { await Future.delayed(Duration.zero); return _onData! .add(subs); } } 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); } } typedef OnData<T> = void Function(T data); class LightSubscription<T> extends StreamSubscription<T> { OnData<T>? _data; }
  • Let’s take a look at the functions of the Rx class

    • Get value adds listening
    • The set value performs the added listen

OBX refresh mechanism

The biggest thing about OBX is that it doesn’t require generics and can refresh automatically when used. How does this work?

  • OBX: There’s not a lot of code, but it’s all useful

    • Obx inherits ObxWidget, and ObxWidget is actually a StatefulWidget
    • The _ObxState code is the core code
class Obx extends ObxWidget { final WidgetCallback builder; const Obx(this.builder); @override Widget build() => builder(); } abstract class ObxWidget extends StatefulWidget { const ObxWidget({Key? key}) : super(key: key); @override _ObxState createState() => _ObxState(); @protected Widget build(); } class _ObxState extends State<ObxWidget> { RxInterface? _observer; late StreamSubscription subs; _ObxState() { _observer = RxNotifier(); } @override void initState() { subs = _observer! .listen(_updateTree, cancelOnError: false); super.initState(); } void _updateTree(_) { if (mounted) { setState(() {}); } } @override void dispose() { subs.cancel(); _observer! .close(); super.dispose(); } Widget get notifyChilds { final observer = RxInterface.proxy; RxInterface.proxy = _observer; final result = widget.build(); if (! _observer! .canUpdate) { 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; } @override Widget build(BuildContext context) => notifyChilds; }

Add to monitor

If a control wants to refresh, there must be some logic to add listening, and then trigger manually somewhere

  • Look at where the _ObxState class adds listeners: only show the code that listens to be added
  • When _ObxState is initialized, an RxNotifier() object is instantiated, and the _observer variable is used to accept: this operation is important
  • One of the key operations in initState is to pass the _updateTree method to the listener method of _observer. The logical body of this method is setState().
class _ObxState extends State<ObxWidget> { RxInterface? _observer; late StreamSubscription subs; _ObxState() { _observer = RxNotifier(); } @override void initState() { subs = _observer! .listen(_updateTree, cancelOnError: false); super.initState(); } void _updateTree(_) { if (mounted) { setState(() {}); }}}

Much of the above logic relates to the RXNotifier class. Take a look at this class

  • RxNotifier is a class that internally instantiates a getStream

    () object and assigns it to Subject
  • The above assignment _updateTree method is passed to the GetStream

    () class and finally added to the _onData list variable
  • Taking a look at the _notifyData method, does it traversal the methods that execute item in the _onData list (item._data)? Call (data)).
class RxNotifier<T> = RxInterface<T> with NotifyManager<T>; mixin NotifyManager<T> { GetStream<T> subject = GetStream<T>(); final _subscriptions = <GetStream, List<StreamSubscription>>{}; bool get canUpdate => _subscriptions.isNotEmpty; StreamSubscription<T> listen( void Function(T) onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) => subject.listen( onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError ?? false, ); } 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>>[]; 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; } 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; }}
  • The above code flow has a little around, the following drew a diagram, I hope to help you

Listening to the transfer

In the _ObxState class, we do a very important operation that listens for object transfers

The _observer object has taken the setState method inside the OBX control, and now we need to move it out!

  • The code to move the object out of _observer is posted below: The main logic is in the notifyChilds method

    • There is a proxy static variable in the RxInterface class. This variable is very important. It is a transit variable!
`class _ObxState extends State<ObxWidget> { RxInterface? _observer; _ObxState() { _observer = RxNotifier(); } Widget get notifyChilds { final observer = RxInterface.proxy; RxInterface.proxy = _observer; final result = widget.build(); if (! _observer! .canUpdate) { 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; } @override Widget build(BuildContext context) => notifyChilds; } abstract class RxInterface<T> { bool get canUpdate; void addListener(GetStream<T> rxGetx); void close(); static RxInterface? proxy; StreamSubscription<T> listen(void Function(T event) onData, {Function? onError, void Function()? onDone, bool? cancelOnError}); }

Several lines of code in Notifychilds are meaningful, so read them line by line

  • Final observer = rxinterface. proxy: Rxinterface. proxy is null, but if it is an intermediate variable, the object is temporarily taken out and stored in the observer
  • RxInterface.proxy = _observer: Assign the address of the RxNotifier() object we instantiated in the _obxState class to RxInterface.proxy

    • Note: Here, the instance of RxNotifier() in RxInterface.proxy has the setState() method of the current OBX control
  • Final Result = Widget.build () : This assignment is important! Call the Widget that we passed in externally

    • If there is a response variable in the Widget, then that variable must be called to get the get value
    • Remember the code for get value?

      mixin RxObjectMixin<T> on NotifyManager<T> { late T _value; T get value { if (RxInterface.proxy ! = null) { RxInterface.proxy! .addListener(subject); } return _value; } } mixin NotifyManager<T> { GetStream<T> subject = GetStream<T>(); }
  • Finally the connection is made. Add the getStream instance from the variable to the RXNotifier () instance in Obx; The RXNotifier () instance has a Subject (getStream) instance, and the change in the data in the RX type triggers the Subject change, resulting in a refresh of the OBX

    mixin NotifyManager<T> { GetStream<T> subject = GetStream<T>(); final _subscriptions = <GetStream, List<StreamSubscription>>{}; bool get canUpdate => _subscriptions.isNotEmpty; void addListener(GetStream<T> rxGetx) { if (! _subscriptions. Either containsKey (rxGetx)) {/ / key GetStream listen method is used to add monitoring method, Final subs = rxGetx.listen((data) {if (! subject.isClosed) subject.add(data); }); final listSubscriptions = _subscriptions[rxGetx] ?? = <StreamSubscription>[]; listSubscriptions.add(subs); }}}
  • if (! _observer! CanUpdate) {} : the judge is very simple, if we don’t have Rx type variables within the Widget, _subscriptions array will be empty, the judge will pass away
  • Rxinterface. proxy = observer: Reassign the original value in rxinterface. proxy to itself, and then the address of _observer object in _ObxState is completed after a magic trip

graphic

conclusion

OBX’s refresh mechanism is quite interesting

  • When a variable is changed, it automatically refreshes its variable OBX control. Other OBX controls do not refresh
  • Use OBX control, do not need to write generic! Bull group!

However, I think the OBX refresh mechanism also has its own defects, from the perspective of its implementation principle, it is unavoidable

  • Because OBX is automatically refreshed, each variable must have its own listening trigger mechanism; So, all of the underlying types, entities, and lists, have to be reencapsulated, and that can have a very serious impact on usage: variable assignment, type calibration, and refresh are all very common and different ways of writing, and for those of you who are not familiar with that, it can be very uncomfortable to read
  • As you can see from the previous code review, there is quite a bit of code that encapsulates all types. Encapsulation types are definitely more expensive than the types that come with DART (this problem can be avoided: wrapping a reactive variable doesn’t necessarily require a lot of code, but here’s a wrapper reference).

Hand rub a state management framework

GetX has two built-in state management mechanisms, which will be rolled out by hand in accordance with its refresh mechanism

I’ll reproduce two classic sets of mechanics in extremely simple code

Dependency injection

  • Before we can do the refresh mechanism, we must first write a dependency injection class, we need to manage the logical layer of those instances ourselves

    • I’ve written a very simple one that only implements three basic functions: injection, fetch, and delete
Class Easy {static T put<T>(T dependency, {String?); static T put<T>(T dependency, {String? tag}) => _EasyInstance().put(dependency, tag: tag); Static T find<T>({String? tag, String? key}) => _EasyInstance().find<T>(tag: tag, key: key); Static bool delete<T>({String? tag, String? key}) => _EasyInstance().delete<T>(tag: tag, key: key); } class _EasyInstance {factory _EasyInstance() => _instance?? = _EasyInstance._(); static _EasyInstance? _instance; _EasyInstance._(); static final Map<String, _InstanceInfo> _single = {}; /// Dependency <T Dependency <T Dependency, {String? tag}) { final key = _getKey(T, tag); // Overwrite _single[key] = _instanceInfo <T>(dependency); return find<T>(tag: tag); } /// get the injected instance T find<T>({String? tag, String? key}) { final newKey = key ?? _getKey(T, tag); var info = _single[newKey]; if (info? .value ! = null) { return info! .value; } else { throw '"$T" not found. You need to call "Easy.put($T())""'; }} // Delete instance bool delete<T>({String? tag, String? key}) { final newKey = key ?? _getKey(T, tag); if (! _single.containsKey(newKey)) { print('Instance "$newKey" already removed.'); return false; } _single.remove(newKey); print('Instance "$newKey" deleted.'); return true; } String _getKey(Type type, String? name) { return name == null ? type.toString() : type.toString() + name; } } class _InstanceInfo<T> { _InstanceInfo(this.value); T value; }
  • Customize a listening class. This class is important, and you need to use both of the following mechanisms
// Listeners < Notifier {Listener <VoidCallback> _Listeners = []; void addListener(VoidCallback listener) { _listeners.add(listener); } void removeListener(VoidCallback listener) { for (final entry in _listeners) { if (entry == listener) { _listeners.remove(entry); return; } } } void dispose() { _listeners.clear(); } void notify() { if (_listeners.isEmpty) return; for (final entry in _listeners) { try { entry.call(); } catch (e) { print(e.toString()); }}}}

EasyBuilder

implementation

  • This pattern requires a custom base class

    • I wrote minimalist here, the related life cycle is not added, this is also very easy to add up, define each life cycle, in the Builder control trigger, it is ok
    • For the sake of code brevity, this will not be shown
class EasyXController { EasyXNotifier xNotifier = EasyXNotifier(); // Refresh the control void Update () {xNotifier.notify(); }}
  • Take a look at the core EasyBuilder control: you’re done!

    • The implementation code is very simple, I hope you can have a clear thinking
Class EasyBuilder<T extends EasyXController> extends StatefulWidget {final Widget Function(T logic) : EasyBuilder<T extends EasyXController> extends StatefulWidget {final Widget Function(T logic) : builder; final String? tag; final bool autoRemove; const EasyBuilder({ Key? key, required this.builder, this.autoRemove = true, this.tag, }) : super(key: key); @override _EasyBuilderState<T> createState() => _EasyBuilderState<T>(); } class _EasyBuilderState<T extends EasyXController> extends State<EasyBuilder<T>> { late T controller; @override void initState() { super.initState(); controller = Easy.find<T>(tag: widget.tag); controller.xNotifier.addListener(() { if (mounted) setState(() {}); }); } @override void dispose() { if (widget.autoRemove) { Easy.delete<T>(tag: widget.tag); } controller.xNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return widget.builder(controller); }}

use

  • It’s easy to use. Let’s look at the logical layer
class EasyXCounterLogic extends EasyXController { var count = 0; void increase() { ++count; update(); }}
  • Interface layer
class EasyXCounterPage extends StatelessWidget { final EasyXCounterLogic logic = Easy.put(EasyXCounterLogic()); @override Widget build(BuildContext context) { return BaseScaffold( appBar: AppBar(title: Const Text('EasyX- Custom EasyBuilder Refresher '), body: Center(Child: EasyBuilder<EasyXCounterLogic>(Builder: (logic) {return Text(' Click ${logic.count} ', style: textStyle (fontSize: 30.0),); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); }}
  • rendering

EBX: Automatic refresh mechanism

If you use Obx instead of using Getx, you will find that the GetXController does not automatically retrieve the injected instances.

Here is a solution for this scenario

implementation

  • In an auto-refresh mechanism, the underlying types need to be encapsulated

    • The main logic is in Rx
    • Set value and get value are the key
// extension IntExtension on int {RxInt get ebs => RxInt(this); } extension StringExtension on String { RxString get ebs => RxString(this); } extension DoubleExtension on double { RxDouble get ebs => RxDouble(this); } extension BoolExtension on bool { RxBool get ebs => RxBool(this); } class RxInt extends Rx<int> {rxInt (int initial) : super(initial); RxInt operator +(int other) { value = value + other; return this; } RxInt operator -(int other) { value = value - other; return this; } } class RxDouble extends Rx<double> { RxDouble(double initial) : super(initial); RxDouble operator +(double other) { value = value + other; return this; } RxDouble operator -(double other) { value = value - other; return this; } } class RxString extends Rx<String> { RxString(String initial) : super(initial); } class RxBool extends Rx<bool> { RxBool(bool initial) : super(initial); } class Rx<T> {easyXNotifier subject = easyXNotifier (); Rx(T initial) { _value = initial; } late T _value; bool firstRebuild = true; String get string => value.toString(); @override String toString() => value.toString(); set value(T val) { if (_value == val && ! firstRebuild) return; firstRebuild = false; _value = val; subject.notify(); } T get value { if (RxEasy.proxy ! = null) { RxEasy.proxy! .addListener(subject); } return _value; }}
  • You need to write a very important transit class, which also stores listener objects for responsive variables

    • This class has a very core logic: it associates response variables with a refresh control!
class RxEasy { EasyXNotifier easyXNotifier = EasyXNotifier(); Map<EasyXNotifier, String> _listenerMap = {}; bool get canUpdate => _listenerMap.isNotEmpty; static RxEasy? proxy; void addListener(EasyXNotifier notifier) { if (! _listenerMap.containsKey(notifier) {// Important: Convert ListenerMap.containsKey (notifier) from EBX to here. .easyXNotifier; // Refresh Notifier.addListener () {// Refresh EBX Listener Notifier.notify(); }); // Add to Map _ListenerMap [notifier] = "; }}}
  • Refresh the control EBX
typedef WidgetCallback = Widget Function();

class Ebx extends StatefulWidget {
  const Ebx(this.builder, {Key? key}) : super(key: key);

  final WidgetCallback builder;

  @override
  _EbxState createState() => _EbxState();
}

class _EbxState extends State<Ebx> {
  RxEasy _rxEasy = RxEasy();

  @override
  void initState() {
    super.initState();

    _rxEasy.easyXNotifier.addListener(() {
      if (mounted) setState(() {});
    });
  }

  Widget get notifyChild {
    final observer = RxEasy.proxy;
    RxEasy.proxy = _rxEasy;
    final result = widget.builder();
    if (!_rxEasy.canUpdate) {
      throw 'Widget lacks Rx type variables';
    }
    RxEasy.proxy = observer;
    return result;
  }

  @override
  Widget build(BuildContext context) {
    return notifyChild;
  }

  @override
  void dispose() {
    _rxEasy.easyXNotifier.dispose();

    super.dispose();
  }
}
  • As mentioned above, in the auto-refresh mechanism, automatic recycling of dependent instances is a cone problem. Here I have written a recycling control to solve this problem

    • When using, must set a layer; If you have a better idea, please let us know in the comments
class EasyBindWidget extends StatefulWidget { const EasyBindWidget({ Key? key, this.bind, this.tag, this.binds, this.tags, required this.child, }) : assert( binds == null || tags == null || binds.length == tags.length, 'The binds and tags arrays length should be equal\n' 'and the elements in the two arrays correspond one-to-one', ), super(key: key); final Object? bind; final String? tag; final List<Object>? binds; final List<String>? tags; final Widget child; @override _EasyBindWidgetState createState() => _EasyBindWidgetState(); } class _EasyBindWidgetState extends State<EasyBindWidget> { @override Widget build(BuildContext context) { return widget.child; } @override void dispose() { _closeController(); _closeControllers(); super.dispose(); } void _closeController() { if (widget.bind == null) { return; } var key = widget.bind.runtimeType.toString() + (widget.tag ?? ''); Easy.delete(key: key); } void _closeControllers() { if (widget.binds == null) { return; } for (var i = 0; i < widget.binds! .length; i++) { var type = widget.binds! [i].runtimeType.toString(); if (widget.tags == null) { Easy.delete(key: type); } else { var key = type + (widget.tags? [i] ?? "); Easy.delete(key: key); }}}}

use

  • Logic layer, this time, we don’t even have to write the base class
class EasyXEbxCounterLogic { RxInt count = 0.ebs; Void increase() => ++count; }
  • Interface layer: The top node of the page has an EasyBindWidget, which can ensure that the dependency injection instance can be automatically recycled
class EasyXEbxCounterPage extends StatelessWidget { final EasyXEbxCounterLogic logic = Easy.put(EasyXEbxCounterLogic());  @override Widget build(BuildContext context) { return EasyBindWidget( bind: logic, child: BaseScaffold( appBar: AppBar(title: const Text('EasyX- custom EBX refresh mechanism ')), body: Center(child: Ebx(() {return Text(' Click ${logic.count.value} ', style: textStyle (fontSize: 30.0),); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ), ); }}
  • rendering

conclusion

These two refresh modes should be automatic refresh mechanism, the idea is very interesting, the response variable and refresh control through the form of static variable connection, cool! Is a Sao operation!

These two sets of state management mechanism, I have given the dependency injection object, automatic recovery of the solution, I hope to enlightening your thinking.

The last

Finally, I have finished the last analysis of the principle of GetX (only for the part of GetX state management), and I have a worry…

  • Some processes are more around, specially drew some diagrams, illustrated always make people happy……

If you read the whole article carefully, you may have noticed that state management + dependency injection can greatly expand the usage scenarios

  • The automatic recovery of GetBuilder is to obtain the injected instance seamlessly with the help of dependency injection, so as to realize the operation of automatic recovery
  • And GetBuilder doesn’t need to pass extra parameters!

Writing down the whole article, I feel really try my best

  • InheritedWidget from InheritedWidget to Path
  • Then it’s dependency injection
  • And then the two states of the framework of the analysis
  • Finally, according to the two refresh mechanisms, two sets of state management framework are set by hand

It is also layers of progressive knowledge, a little bit of display in front of everyone, I hope to help you!!

Series of articles + related addresses

  • The Github address of the Demo in this article is flutter_use
  • Flutter Getx uses the charm of simplicity!
  • The thoughts behind Flutter Bloc, a tangled article
  • The Flutter Provider is the other side of the Flutter Provider.