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:
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
Provides the list of the opensource Flutter apps collection with GitHub repository.