injectable icon indicating copy to clipboard operation
injectable copied to clipboard

Wrong registration order for multiple class implementations

Open Limbou opened this issue 2 years ago • 3 comments

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.

Limbou avatar Dec 20 '21 14:12 Limbou

Got this too 😢

injectable: 1.5.0 injectable_generator: 1.5.2

EmirBostanci avatar Dec 20 '21 14:12 EmirBostanci

Same

injectable: 1.5.3 injectable_generator: 1.5.3 get_it: 7.2.0

MobileLabStudio avatar Jan 23 '22 15:01 MobileLabStudio

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 🙂

VladislavRUS avatar Nov 09 '22 15:11 VladislavRUS