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:

InjectorX

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;
}

/* 
Esse contrato é utilizando o flutter_tripple será exemplificado 
mais para frente 
*/
abstract class IViewModelTriple extends InjetorXViewModelStore<NotifierStore<Exception, int>> {
  Future<void> save(String email, String name);
}

/*
Nesse caso não preciso herdar de Inject, pois o contexto desse objeto 
não precisa controlar suas injeções, contudo o InjetorX poderá injetá-lo onde 
houver necessidade como no exemplo seguinte UserRepoImpl
*/
class ApiImpl implements IApi {
  @override
  Future post(String url, data) async {
    var httpClient = Dio();
    return await httpClient.post(url, data: data);
  }
}

/*
Como nossa implementação do repositório depende do contrato da api, devemos 
herdar da classe Inject para podermos manipular as injeções desse contexto 
separadamente.
 */

class UserRepoImpl extends Inject<UserRepoImpl> implements IUserRepo {
  /*
  No construtor dessa classe não é preciso passar as referências que precisam ser injetadas. 
  Isso é feito um pouco diferente agora, através de Needles 
  (Needle é agulha em inglês). Cada agulha (Ex: Needle<IApi>()) fará a referência 
  necessária ao contrato para o InjectorX saiba o que deve ser injetado no contexto desse
  objeto, pelo no método injector.
  */
  UserRepoImpl() : super(needles: [Needle<IApi>()]);
  /*
  Aqui é definido a variável do contrato da Api que o repositório aceitará para ser
  injetado em seu contexto.
  */
  late IApi api;

  /*
  Quando a classe herda de Inject automaticamente esse método será criado ele terá 
  objeto InjectorX que é um service locator para identificar e referenciar as 
  injeções ao contrato que o IUserRepoImpl precisa.
  */
  @override
  void injector(InjectorX handler) {
    /*
    Aqui de forma abstraída o handler do InjectorX 
    buscará a implementação registada para o contrato IApi
    */
    api = handler.get();
  }

  /*
  Aqui utilizaremos a implementação do contrato em sí, não sabemos qual é 
  a implementação e não precisamos, pois seguindo a regra do contrato imposto isso 
  fica irrelevante.
  */
  @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;
    }
  }
}

/*
Aqui tudo se repetirá como no exemplo anterior, contudo aqui não sabemos 
o que o UserRepoImpl injeta em seu contexto apenas referenciamos ao seu contrato
e o InjectorX saberá o que injetar em cada contexto etapa por etapa.
*/
class UserUsecaseImpl extends Inject<UserUsecaseImpl> implements IUserUsecase {
  UserUsecaseImpl() : super(needles: [Needle<IUserRepo>()]);

  late IUserRepo repo;
  /*
  O conceito de use case é para controlar a regra de negócio de um comportamento em 
  específico nesse caso só deixará salvar usuários com email do 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();
  }
}

 /*
  O ViewModel é responsável pelo controle de estado de uma tela, ou de um widget em específico, note que o 
  view model não controla regra de negócio e sim estado da tela qual for referenciado.
  Nesse caso o estado está sendo controlado por RxNotifier, contudo isso pode ser feito 
  com qualquer outro gerenciador de estado da sua preferência.
 */
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;
  }
}

/* 
 O InjectorX também pode ser integrado com o flutter_triple de maneira simplificada
 facilitando ainda mais o controle de estado por fluxo.
 */
class PresenterViewModel extends NotifierStore<Exception, int>
    with InjectCombinate<PresenterViewModel>
    implements IPresenterViewModel {
  PresenterViewModel() : super(0) {
    /*
    Note que há uma pequena diferença agora temos um init() dentro da chamada do 
    contrutor. Isso ocorre porque ao herdar de InjectCombinate precisa ser iniciado para que o InjectorX saba quais  needles responsáveis pela gerência dos contratos de injeção .
    Para saber mais sobre o flutter_triple acesse: https://pub.dev/packages/flutter_triple
   */
    init(needles: [Needle<IUsecase>()]);
  }
  /*
  No te que referenciamos a dependência diferente agora, não é manipulado mais pelo injector(InjectorX hangles) sim dessa nova maneira referenciado pelo inject()
  */
  IUsecase get usecase => inject();

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

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

/*
Agora partiremos para implementação de uma view para exemplificar o fluxo completo.
O InjectoX tem um recurso específico para lidar com a view.

Nesse primeiro exemplo será usado o ViewModel com RxNotifier;

Observe que agora não é mais implementado o método:
@override
void injector(InjectorX handler) {
  userUsecase = handler.get();
}

Se tratando de uma view isso é feito de maneira diferente. Olhem no intiState() a novo jeito proposto.
*/

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();
    /*
    Aqui agora ao em vez de usar o handler do método injector como exemplificado anteriormente, 
    simplesmente chamamos widget.inject() que terá o service locator da view com os recursos do 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("[email protected]", "Username");
                if (success) {
                  print("Users successful saved");
                } else {
                  print("Error on save user");
                }
              },
              child: Text("Salvar dados do usuário"),
            ),
          )
        ],
      ),
    );
  }
}

/*
Aqui outro exemplo de como podemos implementar com o flutter_triple não há muita diferença em essência
a não ser como lidamos com a mudança de estado.
*/
class ScreenTripleExample extends StatefulWidget
    with InjectCombinate<ScreenTripleExample> {
  ScreenTripleExample() {
     //Não se esqueça de iniciar o 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 que agora é usado o getStore da implementação ViewModel com flutter_triple
        store: viewModel.getStore(),
        onState: (context, state) => Center(
          child: ElevatedButton(
            onPressed: () async {
              await viewModel.save("[email protected]", "Username");
            },
            child: Text("Salvar dados do usuário"),
          ),
        ),
        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 initialization
from the app to show InjectorX which is the implementation of each contract.
Note that at no time is the implementation passed to the constructor of another reference, however
that 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 into memory once and 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 the injection references are passed by constructor, here as it is
a small example we can still see easily, however as
need multiple injections in a single builder and application grows, it will become
chaos and it will be extremely difficult to visualize and control what you are injecting into what.
And in this case, all objects were uploaded into memory even if you don't need to
from that reference it is 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();

Ou instanciando a classe diretamente:


 /* 
 ViewModel depende de IUserUsecase que é implementado por UserUsecaseImpl que
 por sua vez depende de IUserRepo que é implementado por UserRepoImpl que por
 sua vez depende de IApi que é implementado por ApiImpl. Controle de dependência
 é feito em etapas por cada contexto, por isso instanciar a classe diretamente não faz diferença.
 Que mesmo assim tudo que precisa ser injetado nesse contexto será injetado sem problemas.
 */
 
 var viewModel = ViewModelImpl();

Registrado singleton

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);
}

In this way the contract is referenced to the singleton, however this singleton will only be generated one instance if any
underlying object needs your use, otherwise the object will not be put into 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 o parâmetro newInstance: true na referência no Needle<IApi>
  isso quer dizer que mesmo que o InjectorXBind tenha feito o registro 
  desse contrato em singleton, nesse objeto isso será ignorado e sempre trará 
  uma nova instância de 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;
    }
  }
}

Testing and Mock Injection

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

class ApiMock extends Mock implements IApi {}

void main() {

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

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

    when(apiMock.post("", "")).thenAwswer((_) async => true);
    /*
    É utilizado injectMocks da implementação a qual quer testar para substituir as injeções dentro do seu 
    contexto testando e injetando unicamente só o que pertence ao objeto que está mesa de teste, 
    ignorando totalmente tudo que não faz parte desse contexto em específico.
    */
    var userRepoImp = (InjectorXBind.get<IUserRepo>() as UserRepoImpl).injectMocks([NeedleMock<IApi>(mock: apiMock)]);
    var res = await userRepoImp.saveUser("", "");
    expect(res, isTrue);
  });

   /*
   Exemplo por instância;
   */
  test("test use direct implement instance", () async {

    when(apiMock.post("", "")).thenAwswer((_) async => true);
    /*
    É utilizado injectMocks da implementação a qual quer testar para substituir as injeções dentro do seu 
    contexto testando e injetando unicamente só o que pertence ao objeto que está mesa de teste, 
    ignorando totalmente tudo que não faz parte desse contexto em específico.
    Assim a escrita fica mais simplificada contudo tem o mesmo resultado final
    */
    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.

If you want to help with this doc or if you have any questions, please leave your suggestion for improvement

Email: [email protected]