wedge

Recently, WHEN I was writing the Flutter project, I contacted getX framework and was deeply attracted by it. It has powerful functions, simple API and excellent state management mechanism. This article will briefly analyze how GetX can achieve state management through Obx and OBS.

The article is big, you bear with it!

Counter the Demo

First, let’s start with a counter demo and use getX to implement the counter function as follows:

void main() {
  runApp(MaterialApp(
    home: CounterDemoPage(),
  ));
}

class CounterDemoPage extends StatelessWidget {
  var counter = 0.obs;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Counter Demo"),
      ),
      body: Center(
        child: Obx(() => Text("Count =${counter.value}")),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counter.value += 1; }, child: Icon(Icons.add), ), ); }}Copy the code

With the support of getX, CounterDemoPage no longer inherits StatefulWidget and setState() methods, but performs the same functions as the official Demo with the code above.

How does getX accomplish state management with such a simple operation? Let’s take a closer look.

Extend the function obs

We first trace the code obS, which is an extension of int and returns an RxInt object (although it starts with Rx, not the RxDart framework) when we call obS. Getx also extends some of the other basic data types and object types, as described in the getX documentation.

extension IntExtension on int {
  RxInt get obs => RxInt(this);
}

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

There seems to be nothing interesting in the RxInt class other than overwriting the + and – operators, so keep tracing the parent class.

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

  @override
  dynamic toJson() {
    /// Omit extraneous code}}Copy the code

There is only one toJson method implemented in Rx, going further back to _RxImpl:

abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> {
	 _RxImpl(T initial) {
    _value = initial;
  }

  void addError(Object error, [StackTrace? stackTrace]) {
     ///A little...
  }

  Stream<R> map<R>(R mapper(T? data)) => stream.map(mapper);
  
  void update(void fn(T? val)) {
     ///A little...
  }
  
  void trigger(T v) {
    ///A little...}}Copy the code

_RxImpl implements four methods, including addError to send error notifications, update and trigger to update data related methods, and map to encapsulate the map methods of Stream. However, this class inherits RxNotifier (important) and mixes it with RxObjectMixin. Since RxNotifier is too important, let’s look at the mixed RxObjectMixin first:

mixin RxObjectMixin<T> on NotifyManager<T> {
  late T _value;

  /// The refresh data
  void refresh() {
    subject.add(value);
  }

  ///Call set Value and return get value
  T call([T? v]) {
    if(v ! =null) {
      value = v;
    }
    return value;
  }

  /// Indicates the first creation
  bool firstRebuild = true;

  ///. Omit extraneous code
  
  /// Update the data
  set value(T val) {
    /// A little...
  }

  /// To obtainThe value of the _value
  T get value {
    /// # Reservation question 1 This place is important! This place is important!! This place is a priority!! It's so important that it should be repeated for three times.RxInterface.proxy? .addListener(subject);return _value;
  }

  /// Get Strema via subjectStream<T? >get stream => subject.stream;

  ///The following two actions translate the getx class library comments. I did not find the location of this method call, which should be reserved for the developer to call.
  /// Place an existing Stream<T>Bind to this Rx<T>To keep values in sync. (Note translation)
  /// You can bind multiple sources to update values. The subscription is automatically turned off when the Observer widget (GetX or Obx) is unloaded from the widget tree
  void bindStream(Stream<T> stream) {
    final listSubscriptions =
        _subscriptions[subject] ??= <StreamSubscription>[];
    listSubscriptions.add(stream.listen((va) => value = va));
  }
}
Copy the code

You can see that RxObjectMixin is basically all about methods to update data, but how to notify the view when data is updated is not illustrated here, but this class implements NotifyManager. Instead of NotifyManager, let’s go back to _RxImpl. Look at the parent it inherits from.

class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;
Copy the code

The parent class RxNotifier of _RxImpl inherits from RxInterface and blends with NotifyManager as does RxObjectMixin.

Let’s take a look at RxInterface:

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}); /// Avoids an unsafe usage of the `proxy` static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {///Copy the code

RxInterface has four unimplemented methods, canUpdate, addListener, close, and Listen, but none are implemented. The static method notifyChildren has not been called anywhere in the previous trace, so let’s ignore it for now. The static variable proxy appears where the emphasis is, but it must be left until the end.

RxNotifier inherits RxInterface and also inherits four methods implemented in the mixin NotifyManager:

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

  ///According to theWhether _SUBSCRIPTIONS is null determines whether it can be renewed
  bool get canUpdate => _subscriptions.isNotEmpty;
  
  /// # Reservation question 3. This will be introduced later
  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);
    }
  }

  
  /// Question 4 looks like listening to the update of the data, but in the previous analysis, did not find the place to call, so we put it first
  StreamSubscription<T> listen(
    void Function(T) onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) =>
      subject.listen(
        onData,
        onError: onError,
        onDone: onDone,
        cancelOnError: cancelOnError ?? false,);/// Provide the close method to clear the memory externally
  void close() {
    /// A little...}}Copy the code

NotifyManager is the obS inheritance chain that can be traced to the final class. It holds a property subject of type GetStream, which is also the core class of getX framework state management. Subject is useful in addListener and Listen. But let’s hold off for a moment and look at the implementation of Obx.

Customize Widget Obx

After analyzing the obS implementation, let’s look at how the custom Widget Obx is implemented:

typedef WidgetCallback = Widget Function(a);class Obx extends ObxWidget {
  final WidgetCallback builder;

  const Obx(this.builder);

  @override
  Widget build() => builder();
}
Copy the code

The Obx class inherits the ObxWidget, but the functionality of the class is very simple. It just declares a mandatory parameter in the constructor, and the parameter type is a no-parameter function that returns the value of the Widget, and then calls that function directly in the build method that overrides the parent class.

It looks like the core stuff is in Obx’s parent class, ObxWidget, so let’s move on.

abstract class ObxWidget extends StatefulWidget {
  / /... Eliminate useless code
  @override
  _ObxState createState() => _ObxState();

  ///This build method is defined by the GetX framework, not the system method
  @protected
  Widget build();
}
Copy the code

ObxWidget is a Widget that inherits from StatefulWidget. As you know, StatefulWidget needs to have a corresponding State to manage the Widget. Then look at the implementation of _ObxState.

class _ObxState extends State<ObxWidget> {#2 /// So you see this class again
  final _observer = RxNotifier();
  
  #1
  late StreamSubscription subs;

  #3
  @override
  void initState() {
    super.initState();
    ///Here RxNotifier's listen method, the Listen method of NotifyManager, is called, which is where the method is called in question # reserved 4
    subs = _observer.listen(_updateTree, cancelOnError: false);
  }

  #4
  void _updateTree(_) {
    if(mounted) { setState(() {}); }} #5
  @override
  void dispose() {
    subs.cancel();
    _observer.close();
    super.dispose();
  }

  #6
  @override
  Widget build(BuildContext context) =>
      RxInterface.notifyChildren(_observer, widget.build);
}
Copy the code

Here you can see that _ObxState holds two attributes.

# 1 is StreamSubscription Dart provides the class want to know about Steam in Steam, please refer to the Dart | what is a Stream, defines the subscription event the various methods and method. In the current class, StreamSubscription is used to unsubscribe during the #5 Dispose lifecycle to prevent memory leaks.

#2 When we were tracking the obS implementation, we flagged a very important class RxNotifier, and here we meet it again. Every Obx object we create holds an implementation of RxNotifier inside.

In the #3 initState lifecycle of _ObxState, _observer calls the LISTEN method to start listening for data, passing in the _updateTree() function as an argument. When data is updated, # 3_updatetree () is called. This in turn calls the setState method, and when the setState method is called, the #6 Build method is called to return a Widget with updated data, thereby refreshing the interface.

We continue to trace the listen method in #3, and finally in the get_stream.dart class, each listen method creates a LightSubscription object cached in the List attribute? LightSubscription is a subclass of #1 StreamSubscription, which is received by subs in _ObxState.

mixin NotifyManager<T> {
   GetStream<T> subject = GetStream<T>();
  // The Listen method in NotifyManager
   StreamSubscription<T> listen(
      void Function(T) onData, {
      Function? onError,
      void Function()? onDone,
      bool? cancelOnError,
    }) =>
        // Calls the listen method of GetStream directly
        subject.listen(
          onData,
          onError: onError,
          onDone: onDone,
          cancelOnError: cancelOnError ?? false,); }class GetStream<T> {
   List<LightSubscription<T>>? _onData = <LightSubscription<T>>[];
  
   // The listen method in GetStream
   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();return subs;
    }
  
   // Add LightSubscription to the List to be invoked
   FutureOr<void> addSubscription(LightSubscription<T> subs) async {
    if(! _isBusy!) {return_onData! .add(subs); }else {
      await Future.delayed(Duration.zero);
      return_onData! .add(subs); }}}Copy the code

Back to the _ObxState class, in the #6 build method, instead of returning widget.build() directly returns the newly created widget, RxInterface’s static method notifyChildren is called. This is the # reserved problem 2 above. Let’s continue to analyze this method:

static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
  
    final _observer = RxInterface.proxy;
    RxInterface.proxy = observer;
    final result = builder();
    if(! observer.canUpdate) { RxInterface.proxy = _observer;/// This is a variable that uses Obx and does not use the obS modifier, but throws an exception that anyone who has used getx knows 😹
      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;
}
Copy the code

This method is called with a check that if Obx is used as follows, an exception is thrown.

child:Obx(() => Text("Count result"))
///An exception is thrown because there are no observable objects in Obx.
Copy the code

The main reason for this is to avoid memory increases caused by incorrect writing, since Obx inherits from the StatefulWidget, which can be done with the StatelessWidget.

From the above we know that there is a StreamSubscription waiting for notification in the Obx object we created. When will the notification come? Let’s go through the next step.

Obs and Obx binding

After the above analysis, we have a rough idea of the implementation logic of OBS and Obx. How does OBS data end up in Obx? Let’s review your code in the Demo:

Body: Center (child: Obx (() = > Text (" count results = ${counter. Value} ")),)Copy the code

Next trace the counter.value method:

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

Rxinterface.proxy: rxinterface.proxy: rxinterface.proxy: rxinterface.proxy: rxinterface.proxy

② What is the parameter subject?

The RxInt object is derived from NotifyManager, and NotifyManager has a subject property of type GetStream.

Problem ① is difficult, because it starts from the loading of the whole CounterDemoPage, the whole process is as follows:

RxNotifier(1) counter = RxNotifier(1)

2. The build method of CounterDemoPage is called, and an Obx object is created that holds a Function and returns the Widget

3. Obx’s parent ObxWidget and _ObxState are created. _ObxState holds the object RxNotifier(2).

4, _ObxState build method is invoked, call to RxInterface. NotifyChildren method, the p1 holding objects for _ObxState RxNotifier (2), p2 in Obx Function

5. Run notifyChildren

/// The OBS object currently held in Obx
final _subscriptions = <GetStream, List<StreamSubscription>>{};
​
static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
    /// The rxInterface. proxy object is null, and the null value is assigned to_observer
    final _observer = RxInterface.proxy;
    /// The RxNotifier(2) object is copied to rxinterface.proxy, and the proxy is not empty for a short time
    RxInterface.proxy = observer;
    /// Call the Function held in Obx and go to Step 6
    final result = builder();
  
    /// Check whether updates can be made, and if an exception cannot be thrown, check by _Whether subscriptions are empty
    if(! observer.canUpdate) { RxInterface.proxy = _observer;throw ""
    }
  
    /// The proxy object is emptied again to start the next cycle
    RxInterface.proxy = _observer;
    /// The Function return value of Obx is the final return value of this method
    return result;
}
Copy the code

6, the body of the function in Obx is return Text(” count result = ${counter.value}”), which brings us back to where we started in this section, calling counter

Rxinterface. proxy is the RxNotifier(2) in the state _ObxState class of the Obx object.

With # reservation problem 1 resolved, we continue to call the addListener method, which is our # reservation problem 3 above, and we continue to follow up:

/// The OBS object currently held in Obx
final _subscriptions = <GetStream, List<StreamSubscription>>{};

/// This method associates a subject in RxInt with a subject in Obx
/// Parameter rxGetx is subject in RxInt
void addListener(GetStream<T> rxGetx) {
  /// Check whether the current RxInt has been added
  if(! _subscriptions.containsKey(rxGetx)) {/// At this point, RxInt starts listening for changes in value data
    final subs = rxGetx.listen((data) {
      /// Notifications of data changes in RxInt are forwarded directly to GetStream in Obx
      if(! subject.isClosed) subject.add(data); });/// Add a subs of type StreamSubscription to the return value of the LISTEN method in RxInt_SUBSCRIPTIONS, as the basis of canUpdate method
    final listSubscriptions = _subscriptions[rxGetx] ??= <StreamSubscription>[];
    listSubscriptions.add(subs);
  }
}
Copy the code

RxNotifier(1) in RxInt and RxNotifier(2) in Obx trigger the _updateTree function held by RxNotifier(2) when Obx data is updated. Then the build method of _ObxState is redone in setState mode, and finally the UI is updated.

The same logic is performed when the value method of RxInt is triggered when we click the button.

If Obx does not use any Rx object, it will raise an exception during the development phase to alert the developer to the improper operation. The references in RxNotifier(1) and RxNotifier(2) also ensure that messages are not incorrectly notified, which is an interesting design to have to say.

Closing arguments

Finally, the implementation logic of state management mechanism is summarized in a simple way:

1. Create a stream object of type RxInt counter by obS, but it is not subscribed.

2. Create Obx and use counter. When Obx’s build is called for the first time, stream in Obx and stream in counter set subscription events and synchronize the changes observed in counter to Obx.

3. When the stream in Obx observes the change, call the function parameter passed in Obx to complete the UI refresh.

4. Above.

Write in the last

Original is not easy, your attention and praise is my biggest motivation ~