injectable
injectable copied to clipboard
Wrong registration order for multiple class implementations
I am currently using injectable: 1.5.0
and injectable_generator: 1.5.2
.
Here is a link to a fresh Flutter project with the problem highlighted : Link
Importable types
Let's declare following classes:
RepositoryC
:
@singleton
class RepositoryC {
final Networking networking;
RepositoryC(this.networking);
}
Networking
abstract class with implementations for test
and dev/prod
environments:
abstract class Networking {}
@test
@Singleton(as: Networking)
class NetworkingMock implements Networking {}
@dev
@prod
@Singleton(as: Networking)
class NetworkingD implements Networking {
final HttpClient client;
NetworkingD(this.client);
}
HttpClient
class used by dev/prod
Networking
implementation:
@singleton
class HttpClient {
final SharedPreferences sharedPreferences;
HttpClient(
this.sharedPreferences,
);
}
I am using SharedPreferences
here, so let's also declare a RegisterModule
in order to register SharedPreferences
:
@module
abstract class RegisterModule {
@preResolve
@singleton
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
}
After declaring above classes and running build_runner
, this is the result we receive inside injection.config.dart
file:
Future<_i1.GetIt> $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) async {
final gh = _i2.GetItHelper(get, environment, environmentFilter);
final registerModule = _$RegisterModule();
gh.singleton<_i3.Networking>(_i3.NetworkingMock(), registerFor: {_test});
gh.singleton<_i3.RepositoryC>(_i3.RepositoryC(get<_i3.Networking>()));
await gh.singletonAsync<_i4.SharedPreferences>(() => registerModule.prefs, preResolve: true);
gh.singleton<_i3.HttpClient>(_i3.HttpClient(get<_i4.SharedPreferences>()));
gh.singleton<_i3.Networking>(_i3.NetworkingD(get<_i3.HttpClient>()), registerFor: {_dev, _prod});
return get;
}
As you can see, on line 4
of the above code snippet, NetworkingMock
is registered for test
environment.
Right after it, on line 5
we can see registration of RepositoryC
that depends on Networking
.
NetworkingD
, so the dev/prod
implementation of Networking
is registered much lower in the file, on line 8
.
This causes a serious issue on dev/prod
environment, as trying to build the app throws an exception:
Unhandled Exception: 'package:get_it/get_it_impl.dart': Failed assertion: line 372 pos 7: 'instanceFactory != null': Object/factory with type Networking is not registered inside GetIt.
This issue only exists because we are declaring an ImportableType
such as SharedPreferences
as an indirect dependency of NetworkingD
. This order will differ a lot if we replace SharedPreferences
with a different, non importable class.
Let's declare a new class ClassA
and a method returning Future<ClassA>
so we can reach the same type of initialization as for SharedPreferences
:
class ClassA {}
Future<ClassA> getA() async => ClassA();
now let's also include registration of that class inside our RegistrationModule
:
@module
abstract class RegisterModule {
@preResolve
@singleton
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
@preResolve
@singleton
Future<ClassA> get a => getA();
}
and now let's change the type of sharedPreferences
field of HttpClient
class to ClassA
:
@singleton
class HttpClient {
final ClassA sharedPreferences;
HttpClient(
this.sharedPreferences,
);
}
after code generation script we get the following result:
Future<_i1.GetIt> $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) async {
final gh = _i2.GetItHelper(get, environment, environmentFilter);
final registerModule = _$RegisterModule();
await gh.singletonAsync<_i3.ClassA>(() => registerModule.a, preResolve: true);
gh.singleton<_i3.HttpClient>(_i3.HttpClient(get<_i3.ClassA>()));
gh.singleton<_i3.Networking>(_i3.NetworkingMock(), registerFor: {_test});
gh.singleton<_i3.Networking>(_i3.NetworkingD(get<_i3.HttpClient>()), registerFor: {_dev, _prod});
gh.singleton<_i3.RepositoryC>(_i3.RepositoryC(get<_i3.Networking>()));
await gh.singletonAsync<_i4.SharedPreferences>(() => registerModule.prefs, preResolve: true);
return get;
}
As you can see, the order is now as follows:
On line 4
we can see registration of ClassA
,
On line 5
we can see registration of HttpClient
(which was much lower in the file in previous implementation),
On line 6
we can see registration of NetworkingMock
,
On line 7
we can see registration of NetworkingD
(which also was much lower in the file in the previous example),
On line 8
we can see registration of RepositoryC
which depends on Networking
.
Now everything works correctly, both test
and dev/prod
environments will launch and app will be fully functional. It seems like something is wrong with code generation order for ImportableType
's.
Simple data types
Very similar issue to Importable types happens when we declare a simple type (like String or int) inside of RegistrationModule
:
@module
abstract class RegisterModule {
String get url => "https://someurl.com";
}
code inside injection.config.dart
:
_i1.GetIt $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) {
final gh = _i2.GetItHelper(get, environment, environmentFilter);
final registerModule = _$RegisterModule();
gh.singleton<_i3.Networking>(_i3.NetworkingMock(), registerFor: {_test});
gh.singleton<_i3.RepositoryC>(_i3.RepositoryC(get<_i3.Networking>()));
gh.factory<String>(() => registerModule.url);
gh.singleton<_i3.HttpClient>(_i3.HttpClient(get<String>()));
gh.singleton<_i3.Networking>(_i3.NetworkingD(get<_i3.HttpClient>()), registerFor: {_dev, _prod});
return get;
}
I think we can clearly see that there is exactly the same problem here as in the first example, even though there are no ImportableType
's but only simple types. I have not tested it with all possible types, only String
and int
.
I think it is worth to mention, that this issue did not exist in injectable: 1.4.1
and injectable_generator: 1.4.1
. I stumbled upon this problem because my project stopped working after upgrading to latest injectable
version.
Available workarounds before issue is fixed
I believe there are two ways to make the project work:
Remove test
environment implementation for Networking
This would of course cause the RepositoryC
registration to move much lower in the file because it has to be done after at least one registration of Networking
(that's what I observed) and since there is only one after removing NetworkingMock
, it has to go down.
Probably not the best solution if you have tests in your project.
Add importable type dependency to test environment implementation
What I ended up doing is adding SharedPreferences
as a dependency to NetworkingMock
class:
@test
@Singleton(as: Networking)
class NetworkingMock implements Networking {
final SharedPreferences sharedPreferences;
NetworkingMock(
this.sharedPreferences,
);
}
which gave the following result inside generated file:
Future<_i1.GetIt> $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) async {
final gh = _i2.GetItHelper(get, environment, environmentFilter);
final registerModule = _$RegisterModule();
await gh.singletonAsync<_i3.SharedPreferences>(() => registerModule.prefs, preResolve: true);
gh.singleton<_i4.HttpClient>(_i4.HttpClient(get<_i3.SharedPreferences>()));
gh.singleton<_i4.Networking>(_i4.NetworkingMock(get<_i3.SharedPreferences>()), registerFor: {_test});
gh.singleton<_i4.Networking>(_i4.NetworkingD(get<_i4.HttpClient>()), registerFor: {_dev, _prod});
gh.singleton<_i4.RepositoryC>(_i4.RepositoryC(get<_i4.Networking>()));
return get;
}
This looks much better. Of course this is a workaround, because any dependencies inside NetworkingMock
class are unnecessary from testing perspective. This solution will also work with any ImportableType
dependency, as well as a simple type such as String
.
Got this too 😢
injectable: 1.5.0 injectable_generator: 1.5.2
Same
injectable: 1.5.3 injectable_generator: 1.5.3 get_it: 7.2.0
I also encountered this problem, found an acceptable workaround for it:
My class with real functionality is generated with Environment.prod
env:
@GenerateNiceMocks([MockSpec<FilesService>()])
@Singleton(env: [Environment.prod])
class FilesService {
...
I do not mark my mock with any annotations, so it is just:
class FilesServiceMock extends MockFilesService implements FilesService {}
In each test I have the following:
setUp(() async {
await setupTests();
});
Inside setupTests
I put my test mocks to locator:
FilesService mockedFileService = FilesServiceMock();
locator.registerSingleton(mockedFileService);
As a benefit of it you don't get your mocked classes into your build 🙂
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions
I have the same problem