“This is the third day of my participation in the First Challenge 2022. For details: First Challenge 2022”

This article will analyze Getx dependency management through Getx source code, take you step by step to understand Getx dependency management principle, so as to flexibly use Getx dependency injection in the development process.

I have written an article about the integration and use of Getx: Build the Application framework of Flutter. This article describes the use of Getx dependency management. It mainly includes injecting dependencies and obtaining dependencies. The key methods are as follows:

///dependent
Get.put();
Get.lazyPut();
Get.putAsync();
Get.create();

///Access to rely on
Get.find();
Copy the code

Here’s a closer look at how Getx dependency injection works by tracing the source code through these methods.

Get.put

Get. Put is the easiest way to insert a dependency, and its source code is as follows:

  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);
  }
Copy the code

The put method takes four parameters, the last of which is deprecated. The first three parameters were discussed in previous articles: Dependency: Dependency on object instances; Tag: a tag used to distinguish different instances of the same type. Permanent: Indicates whether to retain it permanently. The default value is false.

Put calls _INSERT directly, encapsulates the dependency object dependency as the Builder parameter passed to _INSERT as builder. IsSingleton is true, _INSERT source code is as follows:

void _insert<S>({
    bool? isSingleton,
    String? name,
    bool permanent = false.required InstanceBuilderCallback<S> builder,
    bool fenix = false, {})final key = _getKey(S, name);

    if (_singl.containsKey(key)) {
      final dep = _singl[key];
      if(dep ! =null && dep.isDirty) {
        _singl[key] = _InstanceBuilderFactory<S>(
          isSingleton,
          builder,
          permanent,
          false,
          fenix,
          name,
          lateRemove: dep as_InstanceBuilderFactory<S>, ); }}else {
      _singl[key] = _InstanceBuilderFactory<S>(
        isSingleton,
        builder,
        permanent,
        false, fenix, name, ); }}Copy the code

If name = null, return the type name of the dependent object. If name = null, return the type name of the dependent object. The name of the put method is the tag passed by the _getKey method.

  String _getKey(Type type, String? name) {
    return name == null ? type.toString() : type.toString() + name;
  }
Copy the code

_singl = Map; _singl = Map; _InstanceBuilderFactory = Map;

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

If key does not exist, create _InstanceBuilderFactory. If key does exist, get _InstanceBuilderFactory in _singL and check whether it is null and isDirty is true. True recreates the _InstanceBuilderFactory object and passes the original _InstanceBuilderFactory object to the lateRemove parameter.

MarkAsDirty = markAsDirty; markAsDirty = markAsDirty;

 void markAsDirty<S>({String? tag, String? key}) {
    final newKey = key ?? _getKey(S, tag);
    if (_singl.containsKey(newKey)) {
      final dep = _singl[newKey];
      if(dep ! =null && !dep.permanent) {
        dep.isDirty = true; }}}Copy the code

This method retrieves the _InstanceBuilderFactory object dep by key, determines that deP is not null and permanent is false, and marks isDirty as true, waiting for destruction.

Tracing the source code further reveals that markAsDirty is called in reportRouteWillDispose, which is called just as the route is about to be destroyed, changing the value of the dependent object isDirty.

Getx manages dependencies by packaging the dependency object as _InstanceBuilderFactory and storing it in the Map with a key. If the corresponding key value already exists and is not marked for destruction, the put operation is ignored. Otherwise insert a new _InstanceBuilderFactory object.

Finally, the dependency object passed in is wrapped into the _InstanceBuilderFactory object and put into the _singL Map.

class _InstanceBuilderFactory<S> {
  /// Marks the Builder as a single instance.
  /// For reusing [dependency] instead of [builderFunc]
  bool? isSingleton;

  /// When fenix mode is avaliable, when a new instance is need
  /// Instance manager will recreate a new instance of S
  bool fenix;

  /// Stores the actual object instance when [isSingleton]=true.
  S? dependency;

  /// Generates (and regenerates) the instance when [isSingleton]=false.
  /// Usually used by factory methods
  InstanceBuilderCallback<S> builderFunc;

  /// Flag to persist the instance in memory,
  /// without considering `Get.smartManagement`
  bool permanent = false;

  bool isInit = false;

  _InstanceBuilderFactory<S>? lateRemove;

  bool isDirty = false;

  String? tag;

  _InstanceBuilderFactory(
    this.isSingleton,
    this.builderFunc,
    this.permanent,
    this.isInit,
    this.fenix,
    this.tag, {
    this.lateRemove,
  });

  void _showInitLog() {
    if (tag == null) {
      Get.log('Instance "$S" has been created');
    } else {
      Get.log('Instance "$S" has been created with tag "$tag"'); }}/// Gets the actual instance by it's [builderFunc] or the persisted instance.
  S getDependency() {
    if (isSingleton!) {
      if (dependency == null) {
        _showInitLog();
        dependency = builderFunc();
      }
      returndependency! ; }else {
      returnbuilderFunc(); }}}Copy the code

The key to _InstanceBuilderFactory is the getDependency method, which determines whether it is a singleton or not, and calls builderFunc each time. If it is a singleton, check whether the dependency is null and return. If it is null, call builderFunc.

The builderFunc method is the Builder originally passed in put, which is () => dependency, the dependency passed in by the PUT method.

Put finally calls find and returns the value back. Find gets the dependency and then calls find, which is the same thing as inserting the dependency and getting the dependency right away. That’s why PUT is passing in the entity object of the dependency directly. The builder argument is deprecated because dependent objects are eventually initialized within the PUT method.

Get.find

Find:

  S find<S>({String? tag}) {
    final key = _getKey(S, tag);
    if (isRegistered<S>(tag: tag)) {
      final dep = _singl[key];
      if (dep == 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 ?? dep.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

The whole logic of find is to determine if a dependency is registered and throw an exception if it is not. If the deP is null, throw an exception. If the deP is not null, call _initDependencies to initialize the dependencies and check whether the return value of the initialized dependencies is NULL. If it is not null, it returns directly; if it is empty, the getDependency method is called to get the dependent object instance.

IsRegistered how to determine whether to register, source:

 bool isRegistered<S>({String? tag}) => _singl.containsKey(_getKey(S, tag));
Copy the code

The _getKey method is used to determine whether a key exists. The implementation of the _getKey method was described above.

Follow the source code to see how _initDependencies initialize dependencies:

  S? _initDependencies<S>({String? name}) {
    final key = _getKey(S, name);
    finalisInit = _singl[key]! .isInit; S? i;if(! isInit) { i = _startController<S>(tag: name);if(_singl[key]! .isSingleton!) { _singl[key]! .isInit =true;
        if(Get.smartManagement ! = SmartManagement.onlyBuilder) { RouterReportManager.reportDependencyLinkedToRoute(_getKey(S, name)); }}}return i;
  }
Copy the code

IsInit is false by default. If it’s not initialized, _startController is called. If it’s already initialized, I is not assigned null. Continue tracing _startController source:

  S _startController<S>({String? tag}) {
    final key = _getKey(S, tag);
    finali = _singl[key]! .getDependency()as S;
    if (i is GetLifeCycleBase) {
      i.onStart();
      if (tag == null) {
        Get.log('Instance "$S" has been initialized');
      } else {
        Get.log('Instance "$S" with tag "$tag" has been initialized');
      }
      if(! _singl[key]! .isSingleton!) { RouterReportManager.appendRouteByCreate(i); }}return i;
  }
Copy the code

Get the dependent object by _singl, determine if the dependent object is of type GetLifeCycleBase, call its onStart method if so, and return the dependent object.

GetLifeCycleBase is a mixin and GetxController ends up mixing in GetLifeCycleBase, so this is essentially calling GetxController’s onStart method.

Conclusion: the find method looks for the type and tag dependencies in _singl and initializes them if they are uninitialized, and returns them if they are initialized.

Get.lazyPut

LazyPut is a lazy initialization.

void lazyPut<S>(
    InstanceBuilderCallback<S> builder, {
    String? tag,
    bool? fenix,
    bool permanent = false,
  }) {
    _insert(
      isSingleton: true,
      name: tag,
      permanent: permanent,
      builder: builder,
      fenix: fenix ?? Get.smartManagement == SmartManagement.keepFactory,
    );
  }
Copy the code

Just like the put method, the _insert method is called, except that instead of passing in the instance object directly, it is passed in the builder method that creates the instance. We know from the previous source analysis that the change method is called in the find method. LazyPut does not end up calling find, so it initializes the dependent object the first time it uses find.

Get.putAsync

PutAsync is an asynchronous injection dependency.

  Future<S> putAsync<S>(
    AsyncInstanceBuilderCallback<S> builder, {
    String? tag,
    bool permanent = false,})async {
    return put<S>(await builder(), tag: tag, permanent: permanent);
  }
Copy the code

What is actually called is the PUT method, which is passed in by asynchronously retrieving the value of the Builder.

Get.create

Create the source code:

  void create<S>(
    InstanceBuilderCallback<S> builder, {
    String? tag,
    bool permanent = true,
  }) {
    _insert(
      isSingleton: false,
      name: tag,
      builder: builder,
      permanent: permanent,
    );
  }
Copy the code

The _insert method is also called, similar to the lazyPut method, except that permanent defaults to true.

Get.delete

Delete is used to destroy dependencies. If you use Getx routing management, this method will be called when the page is destroyed without manual call.

  bool delete<S>({String? tag, String? key, bool force = false{})final newKey = key ?? _getKey(S, tag);

    if(! _singl.containsKey(newKey)) { Get.log('Instance "$newKey" already removed.', isError: true);
      return false;
    }

    final dep = _singl[newKey];

    if (dep == null) return false;

    final _InstanceBuilderFactory builder;
    if (dep.isDirty) {
      builder = dep.lateRemove ?? dep;
    } else {
      builder = dep;
    }

    if(builder.permanent && ! force) { Get.log(// ignore: lines_longer_than_80_chars
        '"$newKey" has been marked as permanent, SmartManagement is not authorized to delete it.',
        isError: true,);return false;
    }
    final i = builder.dependency;

    if (i isGetxServiceMixin && ! force) {return false;
    }

    if (i is GetLifeCycleBase) {
      i.onDelete();
      Get.log('"$newKey" onDelete() called');
    }

    if (builder.fenix) {
      builder.dependency = null;
      builder.isInit = false;
      return true;
    } else {
      if(dep.lateRemove ! =null) {
        dep.lateRemove = null;
        Get.log('"$newKey" deleted from memory');
        return false;
      } else {
        _singl.remove(newKey);
        if (_singl.containsKey(newKey)) {
          Get.log('Error removing object "$newKey"', isError: true);
        } else {
          Get.log('"$newKey" deleted from memory');
        }
        return true; }}}Copy the code
  • Get the dependencies first
  • judgment-dependentpermanentTrue is permanent and notforceIf the value is false, the deletion is not mandatoryreturn false
  • Determine whether the dependency isGetxServiceMixinIt is not directly deleted forciblyreturn false.GetxServiceWith theGetxServiceMixin, soGetxServiceCan be retained permanently for the lifetime of the application.
  • Determine whether the dependency isGetLifeCycleBaseThat isGetxControllerCall theironDeleteMethods.
  • iffenixTrue will be the current dependencydependencyAssign to null,isInitSet to false, the key is not deleted, so the next callfindMethod will be called againbuilderCreate dependent instances.
  • iflateRemoveIf it is not null, it is assigned null; otherwise, the key of the current dependency is changed from_singlRemove.

conclusion

By reading the source code of Getx, we found that the essence of Getx dependency management is to save the dependency relationship through a Map, and then search from the Map when we call find method to get the dependency.

Hopefully, this article will give you a deeper understanding of Getx dependency management principles and make use of Getx dependency injection in your development process.