InjectorX
Flutter 的依赖管理
InjectorX 的想法是为了更容易地控制和维护 Flutter 项目中带有 Clean Architecture 的依赖注入。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]