Dependence managment from Flutter

  Flutter App, State Management

InjectorX

Dependence management from Flutter

The idea for InjectorX came about to make it easier to control and maintain dependency injections in a flutter project with Clean Architecture. The main difference InjectorX for the main packages already available is the injection control by context, thus decentralizing the injections and not instantiating what you don’t need outside of that context. In this model, the object itself is a service locator for its own injections, replacing the need to pass injections via the controller, but not losing the code decoupling power, facilitating even more the visualization of what is injected in that object.

Mind Map:

Mind Map

Step by step of what the diagram represents:

First of all we must define our application contracts.

In a contract it is established which rules an object must have when being implemented. So that the underlying objects are not coupled to the implementation itself, but to the contract, being thus independent of the implementation, any object that follows the rules of the contract will be accepted in the referenced injection.

abstract  class  IApi {
   Future < dynamic >  post ( String url, dynamic data);
}

abstract  class  IUserRepo {
   Future < bool >  saveUser ( String email, String name);
}

abstract  class  IUserUsecase {
   Future < bool >  call ( String email, String name);
}

abstract  class  IViewModel {
   Future < bool >  save ( String email, String name);
  bool  get inLoading;
}

/* 
This contract is using flutter_tripple will be exemplified 
later 
*/ 
abstract  class  IViewModelTriple  extends  InjectorXViewModelStore < NotifierStore < Exception , int >> {
   Future < void >  save ( String email, String name);
}

/* 
In this case I don't need to inherit from Inject, because the context of this object 
doesn't need to control its injections, however InjectorX can inject it where 
necessary as in the following example UserRepoImpl 
*/ 
class  ApiImpl  implements  IApi {
   @override 
  Future  post ( String url , data) async {
     var httpClient =  Dio ();
    return  await httpClient. post (url, date : date);
  }
}

/* 
Since our repository implementation depends on the api contract, we must 
inherit from the Inject class so that we can handle the injections from that context 
separately. 
*/

class  UserRepoImpl  extends  Inject < UserRepoImpl >  implements  IUserRepo {
   /* 
  In the constructor of this class it is not necessary to pass the references that need to be injected. 
  This is done a little differently now, through Needles 
  (Needle is needle in English). Each needle (Ex: Needle<IApi>()) will make the 
  necessary 
reference to the contract for the InjectorX to know what should be injected in the context of that   object, through the injector method. 
  */ 
  UserRepoImpl () :  super (needles : [ Needle < IApi >()]);
  /* 
  Here is defined the API contract variable that the repository will accept to be 
  injected into its context. 
  */ 
  late  IApi api;

  /* 
  When the class inherits from Inject automatically this method will be created it will have 
  an InjectorX object which is a service locator to identify and reference the 
  injections to the contract that the IUserRepoImpl needs. 
  */ 
  @override 
  void  injector ( InjectorX handler) {
     /* 
    Here in an abstract way the InjectorX handler 
    will fetch the registered implementation for the IApi contract 
    */ 
    api = handler. get ();
  }

  /* 
  Here we will use the implementation of the contract itself, we don't know what 
  the implementation is and we don't need it, because following the rule of the imposed contract this 
  is irrelevant. 
  */ 
  @override 
  Future < bool >  saveUser ( String email, String name) async {
     try {
       await api
          . post ( "https://api.com/user/save" , { "email" : email, "name" : name});
      return  true ;
    } on  Exception {
       return  false ;
    }
  }
}

/* 
Here everything will repeat as in the previous example, however here we don't know 
what UserRepoImpl injects in its context, we just refer to its contract 
and InjectorX will know what to inject in each context step by step. 
*/ 
class  UserUsecaseImpl  extends  Inject < UserUsecaseImpl >  implements  IUserUsecase {
   UserUsecaseImpl () :  super (needles : [ Needle < IUserRepo > ()]);

  late  IUserRepo repo;
  /* 
  The concept of use case is to control the business rule of a 
  specific 
behavior in that case it will only save users with email from gmail.   */ 
  @override 
  Future < bool >  call ( String email, String name) async {
     if ( email.contains ( "@gmail.com" )) {
       return  await repo. saveUser (email, name);
    } else {
       return  false ;
    }
  }

  @override 
  void  injector ( InjectorX handler) {
    repo = handler. get ();
  }
}

 /* 
  The ViewModel is responsible for controlling the state of a screen, or a specific widget, note that the 
  view model does not control business rules, but the state of the screen which is referenced. 
  In this case the state is being controlled by RxNotifier, however this can be done 
  with any other state manager of your choice. 
*/ 
class  ViewModelImpl  extends  Inject < ViewModelImpl >  implements  IViewModel {
   ViewModelImpl () :  super (needles : [ Needle < IUserUsecase > ()]);

  late  IUserUsecase userUsecase;
  var _inLoading =  RxNotifier ( false );

  set  inLoading ( bool v) => _inLoading.value = v;
  bool  get  inLoading => _inLoading.value;

  @override 
  void  injector ( InjectorX handler) {
    userUsecase = handler. get ();
  }

  @override 
  Future < bool >  save ( String email, String name) async {
     var _result =  false ;

    inLoading =  true ;
    _result =  await  userUsecase (email, name);
    inLoading =  false ;

    return _result;
  }
}

/* 
InjectorX can also be integrated with flutter_triple in a simplified way 
making state control by flow even easier. 
*/ 
class  PresenterViewModel  extends  NotifierStore < Exception , int > 
    with  InjectCombinate < PresenterViewModel > 
    implements  IPresenterViewModel {
   PresenterViewModel () :  super ( 0 ) {
     /* 
    Note there is a slight difference now we have an init() inside the call
    builder. This is because when inheriting from InjectCombinate it needs to be started so that InjectorX knows which needles are responsible for managing the injection contracts. 
    To learn more about flutter_triple go to: https://pub.dev/packages/flutter_triple 
   */ 
    init (needles : [ Needle < IUsecase > ()]);
  }
  /* 
  Since we referenced the dependency differently now, it is no longer handled by the injector(InjectorX hangles) but this new way referenced by the inject() 
  */ 
  IUsecase  get  usecase => inject ();

  @override 
  bool  increment () {
     update (usecase. increment (state));
    return  true ;
  }

  @override 
  NotifierStore < Exception , int >  getStore () {
     return  this ;
  }
}

/* 
Now we're going to implement a view to exemplify the complete flow. 
InjectoX has a specific feature to handle the view.

In this first example the ViewModel with RxNotifier will be used;

Note that the method is no longer implemented: 
@override 
void injector(InjectorX handler) { 
  userUsecase = handler.get(); 
}

When dealing with a view this is done differently. Look in intiState() for the proposed new way. 
*/

class  ScreenExample  extends  StatefulWidget 
    with  InjectCombinate < ScreenExample > {
   ScreenExample () {
      init (needles : [ Needle < IViewModel > ()])
  };
  @override 
  _ScreenExampleState  createState () =>  _ScreenExampleState ();
}

class  _ScreenExampleState  extends  State < ScreenExample > {
   late  IViewModel viewModel;

  @override 
  void  initState () {
     super . initState ();
    /* 
    Here now instead of using the injector method handler as exemplified above, 
    we simply call widget.inject() which will have the view service locator with the resources of InjectorX 
    */ 
    viewModel = widget. inject ();
  }

  @override 
  Widget  build ( BuildContext context) {
     return  RxBuilder (
      builder : (_) =>  IndexedStack (
        index : viewModel.inLoading ?  0  :  1 ,
        children : [
           Center (child :  CircularProgressIndicator ()),
           Center (
            child :  ElevatedButton (
              onPressed : () async {
                 var success = 
                    await viewmodel. save ( "mail@gmail.com" , "Username" );
                if (success) {
                   print ( "Users successful saved" );
                } else {
                   print ( "Error on save user" );
                }
              },
              child :  Text ( "Save user data" ),
            ),
          )
        ],
      ),
    );
  }
}

/* 
Here's another example of how we can implement with flutter_triple there isn't much difference in essence other 
than how we handle state change. 
*/ 
class  ScreenTripleExample  extends  StatefulWidget 
    with  InjectCombinate < ScreenTripleExample > {
   ScreenTripleExample () {
      //Don't forget to start injectorX 
     init (needles : [ Needle < IViewModel > ()])
  };
  @override 
  _ScreenTripleExampleState  createState () =>  _ScreenTripleExampleState ();
}

class  _ScreenTripleExampleState  extends  State < ScreenTripleExample > {
   late  IViewModelTriple viewModel;

  @override 
  void  initState () {
     super . initState ();
    viewModel = widget. inject ();
  }

  @override 
  Widget  build ( BuildContext context) {
     return  Container (
      child :  ScopedBuilder (
         //Note that the ViewModel implementation's getStore is now used with flutter_triple 
        store : viewModel. getStore (),
        onState : (context, state) =>  Center (
          child :  ElevatedButton (
            onPressed : () async {
               await viewmodel. save ( "mail@gmail.com" , "Username" );
            },
            child :  Text ( "Save user data" ),
          ),
        ),
        onError : (context, error) =>  Center (child :  Text (error. toString ())),
        onLoading : (context) =>  Center (child :  CircularProgressIndicator ()),
      ),
    );
  }
}

Contract reference

In order for InjectorX to know what to inject in each needle, we must, at app startup, show InjectorX what the implementation of each contract is. Note that at no point is the implementation passed to the constructor of another reference, no matter how much the implementation has injections in its implementation. This will make all the difference in the injection control, as the visualization is simpler and everything will not be loaded in memory at once, but on demand, as each object needs an injection.

void  _registerDependencies () {
   InjectorXBind . add < IApi > (() =>  ApiImpl ());
  InjectorXBind . add < IUserRepo > (() =>  UserRepoImpl ());
  InjectorXBind . add < IUserUsecase > (() =>  UserUsecaseImpl ());
  InjectorXBind . add < IViewModel > (() =>  ViewModelImpl ());
  InjectorXBind . add< IViewModelTriple > (() =>  ViewModelTriple ());
}

How would this look with GetIt just a simple fictitious example

Note that injection references are passed by constructor, here as a small example we can still visualize it easily, however as you need multiple injections in a single constructor and application grows, it will become chaos and it will be extremely difficult to visualize and control what you’re injecting into what. And in this case, all objects were uploaded into memory even if you don’t need that reference, it’s already in memory.

void  _setup () {
   GetIt . I . registerSingleton < IApi > ( ApiImpl ());
  GetIt . I . registerSingleton < IUserRepo > ( UserRepoImpl ( GetIt . I . get < IApi > () ));
  GetIt . I . registerSingleton < IUserUsecase > ( UserUsecaseImpl ( GetIt . I . get< IUserRepo > () ));
  GetIt . I . registerSingleton < IViewModel > ( ViewModelImpl ( GetIt . I . get < IUserUsecase > () ));
  GetIt . I . registerSingleton < IViewModelTriple > ( ViewModelTriple ( GetIt . I . get < IUserUsecase > () ));
}

InjectoX does not depend on a specific call using the dependency manager reference in GetIt every time we need to retrieve an object that is registered in its package and done as in the example below:

 var viewModel =  GetIt . I . get < IViewModel > ();

If not done as in the example above all the references that need to be auto-injected will not work.

In injectorX I can be free and do it in two ways. Using dependency manager as below:

 IViewModel viewModel =  InjectorXBind . get ();

Or instantiating the class directly:

 /* 
ViewModel depends on IUserUsecase which is implemented by UserUsecaseImpl which 
in turn depends on IUserRepo which is implemented by UserRepoImpl which in 
turn depends on IApi which is implemented by ApiImpl. Dependency control 
is done in steps by each context, so instantiating the class directly makes no difference. 
That even so everything that needs to be injected in this context will be injected without any problems. 
*/
 
 var viewModel =  ViewModelImpl ();

singleton registered

void  _registerDependencies () {
   InjectorXBind . add < IApi > (() =>  ApiImpl (), singleton :  true );
  InjectorXBind . add < IUserRepo > (() =>  UserRepoImpl (), singleton :  true );
  InjectorXBind . add < IUserUsecase > (() =>  UserUsecaseImpl (), singleton :  true );
  InjectorXBind . add <IViewModel > (() =>  ViewModelImpl (), singleton :  true );
  InjectorXBind . add < IViewModelTriple > (() =>  ViewModelTriple (), singleton :  true );
}

This way the contract is referred to the singleton, however this singleton will only be generated an instance if some underlying object needs its use, otherwise the object will not be put in memory.

Instantiating a new object even though it’s registered in singleton

There are 2 ways to do this one is by InjectorXBind as below:

 IViewModel viewModel =  InjectorXBind . get (newInstance :  true );

As in the example above, even having registered in the InjectorXBind as a singleton, this call will bring a new instance of the object;

However this can be done if the Needle of a specific object requires that every time its injections be instantiated again

Ex in the case of IUserRepo:

class  UserRepoImpl  extends  Inject < UserRepoImpl >  implements  IUserRepo {
   /* 
  Note the parameter newInstance: true in the reference in Needle<IApi> 
  this means that even if the InjectorXBind has registered 
  this contract in singleton, in this object it will be ignored and will always bring 
  a new instance of ApiImpl. 
  */ 
  UserRepoImpl () :  super (needles : [ Needle < IApi > (newInstance :  true )]);
  late  IApi api;
  @override 
  void  injector ( InjectorX handler) {
    api = handler. get ();
  }
  @override 
  Future < bool >  saveUser ( String email, String name) async {
     try {
       await api
          . post ( "https://api.com/user/save" , { "email" : email, "name" : name});
      return  true ;
    } on  Exception {
       return  false ;
    }
  }
}

Mock Testing and Injection

Injecting ApiMock into UserRepoImp There are two ways to do this, one InjectorXBind.get and another by instantiating the class directly. In this example I’m using Mockito to build the mocks

class  ApiMock  extends  Mock  implements  IApi {}

void  main () {

  _registerDependencies ();
  
  late  ApiMock apiMock;
  
  setUp (() {
     apiMock =  ApiMock ();
  });

   /* 
   Ex with InjectorXBind.get; 
   */ 
  test ( "test use InjectorXBind.get" , () async {

    When (apiMock. post ( "" , "" )). thenAwswer ((_) async  =>  true );
    /* 
    InjectMocks of the implementation you want to test is used to replace the injections within its 
    context, testing and injecting only what belongs to the object on the test table, 
    totally ignoring everything that is not part of that specific context. 
    */ 
    var userRepoImp = ( InjectorXBind . get < IUserRepo > () as  UserRepoImpl ).injectMocks ([ NeedleMock < IApi > (mock : apiMock)]);
    var res =  await userRepoImp. saveUser ( "" , "" );
    expect (res, isTrue);
  });

   /* 
   Example per instance; 
   */ 
  test ( "test use direct implement instance" , () async {

    When (apiMock. post ( "" , "" )). thenAwswer ((_) async  =>  true );
    /* 
    InjectMocks of the implementation you want to test is used to replace the injections within its 
    context, testing and injecting only what belongs to the object that is on the test table, 
    totally ignoring everything that is not part of that specific context. 
    This way the writing is simplified yet it has the same final result 
    */ 
    var userRepoImp =  UserRepoImpl (). injectMocks ([ NeedleMock <IApi > (mock : apiMock)]);
    var res =  await userRepoImp. saveUser ( "" , "" );
    expect (res, isTrue);
  });
}

This type of mock injection can be done which any object related to InjectorX being them InjectorViewModelTriple, StatefulWidgetInject and Inject, they will all have the same behavior and ease.

Download Flutter Dependence managment from Flutter source code on GitHub

https://github.com/michaelopes/injector_x