InjectorX

Flutter 的依赖管理

InjectorX 的想法是为了更容易地控制和维护 Flutter 项目中带有 Clean Architecture 的依赖注入。InjectorX 与现有的主要包的主要区别在于通过上下文进行注入控制,从而实现注入的去中心化,并且不在该上下文之外实例化您不需要的对象。在此模型中,对象本身是其自身注入的服务定位器,取代了通过控制器传递注入的需要,但又不损失代码解耦能力,从而进一步促进了对该对象中注入内容的视觉化。

思维导图

InjectorX

首先,我们必须定义我们的应用程序契约。

在契约中,规定了对象在实现时必须遵循的规则。这样,底层对象就不会与实现本身耦合,而是与契约耦合,从而独立于实现。任何遵循契约规则的对象都将被接受在引用的注入中。

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

契约引用

为了让 InjectorX 知道在每个注入点注入什么,我们必须在初始化时
从应用程序中告诉 InjectorX 每个契约的实现是什么。
请注意,在任何时候都不会将实现传递给另一个引用的构造函数,但是
实现会在其实现中进行注入。
这将对注入控制产生所有不同,因为可视化更简单,并且所有内容都不会
一次性加载到内存中,而是按需加载,因为每个对象都需要注入。

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

与 GetIt 相比,这将如何体现,只是一个简单的虚构示例

请注意,注入引用是通过构造函数传递的,在这里
这是一个小例子,我们仍然可以轻松地看到,然而
在一个构建器和应用程序中需要多个注入,它会变得
混乱,并且将极难可视化和控制您正在注入的内容。
在这种情况下,即使您不需要
该引用,所有对象都已加载到内存中。

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 不依赖于特定的调用,而是使用依赖管理器引用
在 GetIt 中,我们每次需要检索注册在其包中的对象时都会这样做,就像下面的示例一样

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

如果不像上面的示例那样进行,所有需要自动注入的引用将无法工作。

在 injectorX 中,我可以自由地以两种方式进行。
使用依赖管理器,如下所示

 IViewModel viewModel = InjectorXBind.get();

或者直接实例化类


 /* 
 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();

注册为单例

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

这样,契约就被引用到单例,但是这个单例只会在任何
底层对象需要使用它时才会生成一个实例,否则该对象不会被加载到内存中。

实例化一个新的对象,即使它在单例中注册

有两种方法可以做到这一点,一种是像下面这样的 InjectorXBind

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

就像上面的例子一样,即使 InjectorXBind 被注册为单例,此调用也将带回对象的新实例;

然而,如果特定对象的注入点要求每次都重新实例化其注入,那么这也可以做到

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

测试和模拟注入

将 ApiMock 注入 UserRepoImp
有两种方法可以做到这一点,一种是 InjectorXBind.get,另一种是直接实例化类。
在此示例中,我使用 Mockito 来构建模拟

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

这种类型的模拟注入可以与任何与 InjectorX 相关的对象进行,它们是 InjectorViewModelTriple、StatefulWidgetInject 和 Inject,它们都将具有相同的行为和便捷性。

如果您想帮助撰写此文档或有任何疑问,请留下您的改进建议

电子邮件: [email protected]