preface

Prejudice in the mind is a mountain, no matter how hard you try, can not be moved.

This is a quote from 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 years old job crisis, big factory card bachelor’s degree, no house without a car is difficult to get married and so on, so this sentence is often mentioned.

At the same time, the GetX framework has been dogged by stereotypes due to comments made by GetX authors.

I’m not writing this article to justify GetX

  • I asked myself that I wasn’t a big fan of any of the state frameworks, Provider and Bloc, and I wrote about their use, principles, and code generation plug-ins
  • In my mind, this kind of framework is not so mysterious
  • Because I am familiar with its principles, it is relatively easy to get started and use it, so there is not much time cost to switch related frameworks
  • So I don’t have to be a moral

GetX overall design, there are many excellent ideas, I hope to show these excellent design ideas to you; It may help you to design your own framework and keep a record of your own thinking process.

Front knowledge

Before we get to GetX, we need to introduce a few things that have contributed to the development of Flutter

InheritedWidget

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

  • Bao Dao dragon, orders the world, dare not from, lean on the day not out, who and contend
  • Relying on the sword of Heaven, the sword hides the Nine Shades of Truth
  • Dragon slaying knife, knife collection “Dragon Eighteen Palms”, “Wumu will”

What does the InheritedWidget stash?

  • Dependent nodes, data transfer
  • Fixed point refresh mechanism

The data transfer

[InheritedWidget] [InheritedWidget] [InheritedElement] [InheritedWidget] [data transfer

Save the data

  • InheritedWidget stores data, which is a relatively simple operation, in an InheritedElement
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);

  ///Initialize whatever you want, make it read only, okay
  String value = 'Transfer data';
}
Copy the code

Take the data

  • Any child of TransferDataWidget (a subclass of the InheritedWidget above) can seamlessly fetch data via the BuildContext of the child
var transferDataElement = context.getElementForInheritedWidgetOfExactType<TransferDataWidget>()
            asTransferDataElement? ;var msg = transferDataElement.value;
Copy the code

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

  • It can be found that we got the XxxInheritedElement instance and then got the stored value, so the key isgetElementForInheritedWidgetOfExactType()This method
    • The code is very simple. It takes values from the _inheritedWidgets map, and the 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];returnancestor; }... }Copy the code
  • Then just figure out how _inheritedWidgets are stored, and everything will be clear
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; }}Copy the code

The overall logic is still relatively clear

  1. When a node is encounteredInheritedWidgetThe _inheritedWidgets variable of the parent node is given to the incomingWidgets temporary variable
    1. IncomingWidgets empty: The _inheritedWidgets variable of Element’s parent class instantiates a map object
    2. IncomingWidgets are not empty: Return a new Map instance with all the data on the parent _inheritedWidgets, and assign the value to the _inheritedWidgets variable of the parent Element
  2. Assigns an instance of itself to the parent Element’s _inheritedWidgets variable with key as runtimeType for its widget

Why do we use the _inheritedWidgets variable in an Element instance of any Widget to take the InheritedElement instance directly to the parent?

  • Element has a parent node assigned 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;
    }

	...
}
Copy the code
  • graphic

The refresh mechanism

There’s some interaction between InheritedElement and Element, and it actually comes with a refresh mechanism

  • InheritedElement stores the 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); }}Copy the code
  • InheritedElement refreshes child Elements
    • NotifyClients Removes all elements stored in a _dependents method and passes in notifyDependent
    • In the notifyDependent method, passing Element calls its own didChangeDependencies() method
    • Element’s didChangeDependencies() method refreshes itself by calling markNeedsBuild()
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(); }... }Copy the code

How do inheritedWidgets’ children define themselves as elements

What about the _dependents variable added to the InheritedElement?

  • Element has a dependOnInheritedElement method
    • The dependOnInheritedElement method in Element passes in the InheritedElement instance ancestor
    • Ancestor calls the updateDependencies method, passing in its Element instance
    • InheritedElement adds this Element to the _dependents variable
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);
    returnancestor.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); }}Copy the code
  • DependOnInheritedElement This method is also easy to use
    • In general use in a Widget InheritedElement getElementForInheritedWidgetOfExactType access to the parent node
    • Then pass it into the dependOnInheritedElement method
/ / for
var inheritedElement = context
            .getElementForInheritedWidgetOfExactType<ChangeNotifierEasyP<T>>()
        asEasyPInheritedElement<T>? ; context.dependOnInheritedElement(inheritedElement);Copy the code

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

To learn more about the principles of Provider, see the following articles

  • The Other side of the Flutter Provider

graphic

  • Take a look at an illustration of the InheritedWidget refresh mechanism

Routing tips

  • Route Navigator is basically some static methods to operate routes, NavigatorState is the implementation of the specific logic

If you’re using an InheritedWidget to get data, you probably have A problem with an A page > B page > C page

If I use an InheritedWidget to store data on page A and jump to page B or PAGE C, I will find that the InheritedElement of page A cannot be retrieved using the context

This aspect proves that Navigator route redirects: page A redirects 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

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

For more information about the principles of Flutter routing, see this article

thinking

[InheritedWidget] gives us a lot of convenience

  • We can get the InheritedElement we want within the Widget tree (by generics)
  • InheritedElement and Element interactions also implement a very clean refresh mechanism
  • Do some deep encapsulation and even manage resource release for many controls seamlessly

However, the way Element provides to obtain InheritedElement does not combine well with the routing mechanism after all. This is also inevitable in module design, perhaps the best solution of some module design, it is difficult to worry about some mechanisms of other modes

The InheritedWidget is a very useful tool in our learning about Flutter

  • But as the requirements get more complex, your skills get better and better
  • The dragon slayer, perhaps gradually, is not for you
  • Sometimes it even limits your moves
  • A blacksmith who makes magic weapons, in his mind, the best magic weapon, may always be the next

Most state management frameworks separate the interface layer from the logical layer, which handles the refresh of the interface. The logical layer can be handed over to the InheritedWidget for storage management; Explanation, we also can store our own management!

  • By managing the logical layer yourself, you are free from the constraints of the Element tree and not trapped in the parent nodes of the Element tree
  • It is easy to access the logic layer of the previous page, the next page or the previous page in the route to the page.

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

Dependency injection

instructions

Dependency injection is implemented as follows (Wikipedia) :

  • Interface-based. Implements specific interfaces for external containers to inject objects of dependent types.
  • Based on set method. Implement a public set method for a specific property to let the external container call in an object of the dependent type.
  • Based on constructors. Implements a constructor for a particular parameter that passes in an object of the dependent type when creating a new object.
  • Based on annotations. Java-based annotations, such as “@autoWired” in front of a private variable, allow external containers to pass in the corresponding object without explicitly defining the above three types of code. This scheme is equivalent to defining a public set method, but because there is no real set method, it does not expose interfaces that should not be exposed for dependency injection (because the set method only wants to be accessed by the container for injection and does not want other dependent objects to access it).

Strongly coupled type, based on constructors

class Test {
  String msg;

  Test(String msg) {
    this.msg = msg; }}Copy the code

The set method

class Test {
  String? _msg;

  void setMsg(String msg) {
    this._msg = msg; }}Copy the code

In Java, if you pass a value directly to the constructor, you need more and more values, so you need to add the constructor parameters. Because of the strong coupling of many classes, you can change the constructor.

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

  • Introduce GetX as an intermediate layer for management
    • Looking at the figure below, the broker pattern comes to mind immediately
    • This is also the idea of inversion of control.

Put

Take a look at GetX injection

  • Put to use
var controller = Get.put(XxxGetxController());
Copy the code
  • Look inside
    • Ah, all kinds of SAO operation
    • 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);
}
Copy the code
  • The main logic seems to be in GetInstance
    • You can look at the implementation of the local singleton, I found that a lot of source code is written in this way, very concise
    • Global data is stored in _singL, which is a Map
      • Key: runtimeType of the object or Type + tag of the class
      • Value: _InstanceBuilderFactory class, where we pass in the dependedt object
    • The _singL map is saved with putIfAbsent instead of PUT
      • If the map contains the same data as the incoming key, the incoming data will not be stored
      • That is, objects of the same class instance will not be overwritten, only the first data will be stored and subsequently discarded
    • Finally, use the find method to return the instance passed in
class GetInstance {
  factoryGetInstance() => _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);
      returni ?? _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())"; }}}Copy the code

find

  • The find method is fairly simple, just fetching data from a map
S find<S>({String? tag}) => GetInstance().find<S>(tag: tag);
Copy the code
  • Let’s look at the logic
    • Check whether _singL contains data of the key. If yes, get the key. If no, throw an exception
    • Key code: _singl[key]! .getDependency() as S
class GetInstance {
  factoryGetInstance() => _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);
      returni ?? _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())"; }}}Copy the code

GetBuilder refresh mechanism

use

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

  • Logic layer
class GetCounterEasyLogic extends GetxController {
  var count = 0;

  voidincrease() { ++count; update(); }}Copy the code
  • interface
class GetCounterEasyPage extends StatelessWidget {
  final GetCounterEasyLogic logic = Get.put(GetCounterEasyLogic());

  @override
  Widget build(BuildContext context) {
    return BaseScaffold(
      appBar: AppBar(title: const Text('Counter - Simple')),
      body: Center(
        child: GetBuilder<GetCounterEasyLogic>(builder: (logic) {
          return Text(
            'click on the${logic.count}Time ',
            style: TextStyle(fontSize: 30.0)); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); }}Copy the code

GetBuilder

One day, I was lying in bed thinking

  • The Obx state management, GetXController instance collection is placed in the routing, which has some limitations in many scenarios
  • Then IT occurred to me that GetBuilder uses a generic, and that’s where you get the GetxController instance, and GetBuilder is a StatefulWidget
  • This way you can use it to recycle instances, which can solve the problem that GetXController instances cannot be recycled in many scenarios (without using Getx routing).
  • I excitedly opened the Getx project, ready to raise PR, and then found that GetBuilder had written the operation of recycling instance in Dispose
  • Dubious!

Built-in recycle mechanism

  • There is a lot of code simplification here, showing only the code for the reclamation 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!);
  }
}
Copy the code

The logic in the code is quite clear, initState get instance, Dispose dispose dispose instance

  1. Get the corresponding GetXController instance via generics on GetBuilder
    • Not present: instance passed in using init
    • 2. To use directly; Init Passed an invalid instance
  2. AutoRemove controls whether the GetXController instance is automatically reclaimed
    • Default value: true: Automatic reclamation is enabled by default
    • True: enable automatic reclamation false: disable automatic reclamation

Refresh the logic

  • Only code related to the refresh logic is preserved here, and code that doesn’t need attention 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() {
    varnewFilter = 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!);
  }
}
Copy the code

Key steps

  1. Get the injected GetXController instance via generics
  2. Add listening code
    • AddListener: Adds a listening callback
    • AddListenerId: To add a listening callback, the id must be set and the corresponding ID must be written during update
  3. Listening code: the core code is the getUpdate method, which is in GetStateUpdaterMixin
    • The getUpdate() logic is setState(), which refreshes the current GetBuilder

graphic

Update

  • The triggering logic is simple enough to use update
    • Ids: corresponds to the Getbuilder above, can refresh the corresponding set ID of the Getbuilder
    • Condition: whether to refresh a condition. Default is true (if 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 inids) { refreshGroup(id); }}}}Copy the code
  • Take a look at the key method refresh(), in the ListNotifier class
    • As you can see, generics in _updaters is a method
    • The listener added to GetBuilder is a method parameter, and inside the method body is setState().
    • His live! GetBuilder add methods (method body is setState), update traversal triggers all add methods
typedef GetStateUpdate = void Function(a);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!();
    }
  }

  ...
}
Copy the code
  • Refresh group = refresh; refresh group = refresh; refresh group = refresh
    • Iterate over all 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 inids) { refreshGroup(id); }}}}class ListNotifier implements Listenable {
  HashMap<Object?.List<GetStateUpdate>>? _updatersGroupIds =
      HashMap<Object?.List<GetStateUpdate>>();

  void _notifyIdUpdate(Object id) {
    if(_updatersGroupIds! .containsKey(id)) {finallistGroup = _updatersGroupIds! [id]! ;for (var item inlistGroup) { item(); }}}@protected
  void refreshGroup(Object id) {
    assert(_debugAssertNotDisposed()); _notifyIdUpdate(id); }}Copy the code

conclusion

  • Take a look at the GetBuilder refresh icon

Obx refresh mechanism

This refresh mechanism differs a little from the state management framework (Provider, Bloc) and GetBuilder above

  • Variables: Base types, entities, lists and other data types, the author encapsulates a set of Rx types, quickly add obS after the data

    • RxString MSG = “test”.obs (var MSG = “test”.obs)
  • Update: The base type updates the data directly, the entity needs to be.update()

  • Use: to use this type of variable, usually with.value, the author also gives a shortcut variable after the ().

    • I do not recommend the form of plus (), which is too unfriendly to the people of follow-up maintenance projects

The most interesting Obx refresh mechanism is that when a variable is changed, the Obx surrounding it automatically refreshes. Note that only the Obx surrounding this variable is refreshed! Other OBXs do not refresh.

How does this work?

  • In fact, it’s quite simple to implement
  • However, if there is no contact with this idea, I am afraid that it is difficult to scratch your head, but also to play so…

use

A quick look at usage

  • logic
class GetCounterRxLogic extends GetxController {
  var count = 0.obs;

  ///Since the increase
  void increase() => ++count;
}
Copy the code
  • view
class GetCounterRxPage extends StatelessWidget {
  final GetCounterRxLogic logic = Get.put(GetCounterRxLogic());

  @override
  Widget build(BuildContext context) {
    return BaseScaffold(
      appBar: AppBar(title: const Text('counter - responsive')),
      body: Center(
        child: Obx(() {
          return Text(
            'click on the${logic.count.value}Time ',
            style: TextStyle(fontSize: 30.0)); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); }}Copy the code

Rx class variables

Here, RxInt is used as an example to take a look at its internal implementation

  • Obs = RxInt(0) obs = RxInt(0)
extension IntExtension on int {
  /// Returns a `RxInt` with [this] `int` as initial value.
  RxInt get obs => RxInt(this);
}
Copy the code
  • RxInt: This makes it clear that running with.value automatically returns a current instance and changes its 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; }}Copy the code
  • Look at the parent classRx
    • 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]'; }}}Copy the code
  • The _RxImpl class inherits RxNotifier and with RxObjectMixin
  • The content of this class is relatively large, mainly RxNotifier and RxObjectMixin content
  • There’s a lot of code, so let me show you the whole code; This will be simplified in the next description
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); });finallistSubscriptions = _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));
  }
}
Copy the code
  • Simplifying _RxImpl, there’s a lot of stuff going on here, so I’m going to simplify it a little bit and show you what I need to focus on: there are a few things to focus on here
    • RxInt is a data type with built-in callback (GetStream)
    • When the value variable of RxInt changes (set value), subject.add(_value) is triggered, and the internal logic is automatic refresh
    • RxInt gets value, and a listener will be added. This is very important.
abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> {

  void update(voidfn(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); });finallistSubscriptions = _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; }}Copy the code
  • Why GetStream’s Add has a refresh operation: It deletes a lot of code and keeps important code
    • When the Add method is called, the _notifyData method is called
    • The _notifyData method iterates through the _onData list and executes its generic _data method based on the condition
    • My guess is that 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;
}
Copy the code
  • So here’s what the Rx class does
    • Get value Adds listening
    • Set value Performs added listening

Obx refresh mechanism

What makes Obx special is that it doesn’t require generics and can refresh automatically. How does this work?

  • Obx: There’s not a lot of code, but it’s all useful
    • Obx inherits ObxWidget, which is actually a StatefulWidget
    • The code in _ObxState 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
  voidinitState() { subs = _observer! .listen(_updateTree, cancelOnError:false);
    super.initState();
  }

  void _updateTree(_) {
    if(mounted) { setState(() {}); }}@override
  voiddispose() { 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;
}
Copy the code

Add to monitor

When a control wants to refresh, there must be some logic to add a listener and trigger it manually somewhere

  • Take a look at where the _ObxState class adds listeners: just show the code that adds listeners

  • When _ObxState is initialized, an RxNotifier() object is instantiated, using the _observer variable to accept: this operation is important

  • A critical operation is done in initState. In the listener method of _observer, the _updateTree method is passed in. The logical body of this method is setState().

class _ObxState extends State<ObxWidget> {
  RxInterface? _observer;
  late StreamSubscription subs;

  _ObxState() {
    _observer = RxNotifier();
  }

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

  void _updateTree(_) {
    if(mounted) { setState(() {}); }}}Copy the code

Much of the above logic is related to the RxNotifier class, so take a look at this class

  • The RxNotifier class internally instantiates a GetStream() object and assigns it to the Subject
  • The above assignment of _updateTree is passed into the GetStream() class, which eventually adds _onData to the list variable
  • Take a look at the _notifyData method. Is it iterating over methods that execute items 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? getlength => _onData? .length;bool gethasListeners => _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}) {
    finalsubs = LightSubscription<T>( removeSubscription, onPause: onPause, onResume: onResume, onCancel: onCancel, ) .. onData(onData) .. onError(onError) .. onDone(onDone) .. cancelOnError = cancelOnError; addSubscription(subs); onListen? .call();returnsubs; }}Copy the code
  • The above code process is a little round, the following draw a picture, I hope to help you

Listening to the transfer

An important operation is done in the _ObxState class to listen for object migration

The object in _observer has taken the setState method inside the Obx control and now needs to transfer it out.

  • Here is the code for moving objects out of _observer: The main logic is in notifyChilds
    • The RxInterface class has a proxy static variable that is very important. It is a relay 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});
}
Copy the code

Several lines of notifyChilds make sense. Read them line by line

  • Final Observer = rxinterface. proxy: rxinterface. proxy: rxinterface. proxy: rxinterface. proxy: RxInterface

  • Rxinterface.proxy = _observer: assigns the address of the RxNotifier() object we instantiated in the _ObxState class to rxinterface.proxy

    • Note: Here, the RxNotifier() instance in rxinterface.proxy has the setState() method of the current Obx control
  • Final result = widget.build() : This assignment is quite important! Call the Widget we passed in externally

    • If the Widget has a reactive variable, it must be called to get a value from that variable

    • 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>();
      }
      Copy the code
    • Add the GetStream instance to the RxNotifier() instance in Obx. RxNotifier() has an instance of Subject (GetStream), and data changes in type Rx trigger subject changes that eventually refresh 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.containsKey(rxGetx)) {// The listen method is used to add listener methods to GetStream
            final subs = rxGetx.listen((data) {
              if(! subject.isClosed) subject.add(data); });finallistSubscriptions = _subscriptions[rxGetx] ?? = <StreamSubscription>[]; listSubscriptions.add(subs); }}}Copy the code
  • if (! _observer! CanUpdate) {} : This judgment is very simple, if the Widget we passed didn’t have a Rx variable, the _SUBSCRIPTIONS array would be empty and this judgment would not pass

  • Proxy = observer: RxInterface. Proxy = observer: RxInterface. Proxy = observer: RxInterface

graphic

conclusion

Obx refresh mechanism, or quite interesting

  • Rx variable changes, automatic refresh wrap its variable Obx control, other Obx control does not refresh
  • Use Obx controls, no need to write generics! Bull group!

However, I think the Obx refresh mechanism has its own flaws, and from the perspective of how it is implemented, it is bound to generate these problems

  • Because of Obx’s automatic refresh, each variable must have its own listening trigger mechanism; As a result, all of the base types, entities, and lists need to be rewrapped, which can have serious usage implications: variable assignments, type demarcations, and refreshes are different from normal writing, which can be very uncomfortable for people who are not familiar with it
  • Because of the reencapsulation of all types, after the code review above, you also found that there is quite a lot of code to encapsulate types; Encapsulating types is definitely more resource-intensive than dart’s native types. (This problem can be avoided: encapsulating a reactive variable doesn’t necessarily require a lot of code, but I’ll give you a reference below.)

Hand rub a state management framework

GetX has two state management mechanisms built into it, and this one will follow its refresh mechanism

I’m going to reproduce two classic mechanics in extremely simple code

Dependency injection

  • Before we can do the refresh mechanism, we must first write a dependency injection class, and we need to manage those instances of the logical layer ourselves
    • Here I write a very simple, only three basic functions: injection, get, delete
///Dependency injection is a class that external instances can be injected into and managed by
class Easy {
  ///Into the instance
  static T put<T>(T dependency, {String? tag}) =>
      _EasyInstance().put(dependency, tag: tag);

  ///Gets an instance of the injection
  static T find<T>({String? tag, String? key}) =>
      _EasyInstance().find<T>(tag: tag, key: key);

  ///Delete the instance
  static bool delete<T>({String? tag, String? key}) =>
      _EasyInstance().delete<T>(tag: tag, key: key);
}

///The specific logic
class _EasyInstance {
  factory_EasyInstance() => _instance ?? = _EasyInstance._();static _EasyInstance? _instance;

  _EasyInstance._();

  static final Map<String, _InstanceInfo> _single = {};

  ///Into the instance
  T put<T>(T dependency, {String? tag}) {
    final key = _getKey(T, tag);
    // Save only the first injection: optimized for auto-refresh, data is not reset every time a hot reload occurs
    _single.putIfAbsent(key, () => _InstanceInfo<T>(dependency));
    return find<T>(tag: tag);
  }

  ///Gets an instance of the injection
  T find<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    var info = _single[newKey];

    if(info? .value ! =null) {
      returninfo! .value; }else {
      throw '"$T" not found. You need to call "Easy.put($T(), "" '; }}///Delete the 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;
}
Copy the code
  • A custom listener class is important for both of the following mechanisms
///Custom listening trigger class
class EasyXNotifier {
  List<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()); }}}}Copy the code

EasyBuilder

implementation

  • This pattern requires a custom base class
    • This is easy to add up. Define each life cycle and trigger it in the Builder control
    • For the sake of brevity, I’ll leave this table out
class EasyXController {
  EasyXNotifier xNotifier = EasyXNotifier();

  ///Refresh the controls
  voidupdate() { xNotifier.notify(); }}Copy the code
  • Take a look at the core EasyBuilder control: That’s it!
    • Implementation of the code to write extremely simple, I hope you can have a clear idea
///Refresh control, its own recycling mechanism
class 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) {
    returnwidget.builder(controller); }}Copy the code

use

  • It’s easy to use. Let’s look at the logic layer
class EasyXCounterLogic extends EasyXController {
  var count = 0;

  voidincrease() { ++count; update(); }}Copy the code
  • 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 Refresh Mechanism ')),
      body: Center(
        child: EasyBuilder<EasyXCounterLogic>(builder: (logic) {
          return Text(
            'click on the${logic.count}Time ',
            style: TextStyle(fontSize: 30.0)); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); }}Copy the code
  • rendering

Ebx: Automatic refresh mechanism

The automatic refresh mechanism is not used to determine which injection instances are used internally because there are no generics. Getx reclaims these instances in the route. However, if you use Obx instead of Getx route, you will find that GetXController does not automatically reclaim these instances.

Here’s a solution for this scenario

implementation

  • In an automatic refresh mechanism, the underlying types need to be encapsulated
    • The main logic is in Rx
    • Set Value and get value are key
///Expanding function
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);
}

///Package types
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);
}

///The main logic
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; }}Copy the code
  • You need to write a very important relay class, which also stores listeners for reactive variables
    • This class has very core logic: it associates reactive variables with refresh controls!
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)) {// Refresh in variable listener
      notifier.addListener(() {
        // Refresh the listener added in ebX
        easyXNotifier.notify();
      });
      // Add to map
      _listenerMap[notifier] = ' '; }}}Copy the code
  • Refresh control Ebx
typedef WidgetCallback = Widget Function(a);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(); }}Copy the code
  • As mentioned above, automatic recycling of dependent instances is a cone problem in the auto-refresh mechanism, and here I have written a recycling control that solves this problem
    • When used, must set a layer; If you have a better idea, please let me 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++) {vartype = widget.binds! [i].runtimeType.toString();if (widget.tags == null) {
        Easy.delete(key: type);
      } else {
        varkey = type + (widget.tags? [i] ??' '); Easy.delete(key: key); }}}}Copy the code

use

  • Logic layer, this time, we don’t even need to write the base class
class EasyXEbxCounterLogic {
  RxInt count = 0.ebs;

  ///Since the increase
  void increase() => ++count;
}
Copy the code
  • Interface layer: The top node of the page has an EasyBindWidget that ensures that dependency injection instances can be recycled automatically
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 on the${logic.count.value}Time ',
              style: TextStyle(fontSize: 30.0)); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ), ); }}Copy the code
  • rendering

conclusion

These two kinds of refresh mode, high value, should be automatic refresh mechanism, the idea is very interesting, responsive variable and refresh control through the form of static variables to establish a connection, cool! Another SAO operation!

These two sets of state management mechanism, I have given the dependency injection object, automatic recycling solution, hope to enlightening everyone’s thinking.

The last

Finally, the last GetX principle analysis is finished (only for GetX state management this part of the content), a worry…

  • Some process is more around, specially drew some pictures, graphic always let a person feel happy……

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

  • GetBuilder automatic collection is the use of dependency injection, seamless access to injection instances, so as to achieve automatic collection operations
  • And GetBuilder doesn’t need to pass any extra parameters!

I really tried my best to write the whole article

  • From InheritedWidget to Route
  • Then it’s dependency injection
  • Then two kinds of state frame principle analysis
  • Finally, according to the two refresh mechanisms, hand rub two sets of state management framework

It is also a step by step will be one of the knowledge, a little bit of display in front of you, I hope to help you!!

Series of articles + related address

  • The Github address of Demo in this article is flutter_use

  • Use Flutter GetX — simple charm!

  • Source text: The idea behind the Flutter Bloc, a tangled article

  • The other side of the Flutter Provider