1

Problem

I am building an app with a dynamic feature.

To provide all the dependencies to the main module and the feature module I am using dagger 2. The feature component is depending on the main component and because of that, the feature component is having a different scope than the main component scope (@Singleton in that case)

One of the interface injected in the main module are implemented on the feature module and provided by reflection in the main module. The implementation is also used in the feature module.

The problem that I have is that the instance provided in the main module is different from the one in the feature module (because of the scopes) but I would like to have just one instance provided with dagger.

Code

Here some code and you can find the whole example project in github

Dagger configuration for the main module:

TestModule.kt

@Module
class TestModule {

    @Provides
    @Singleton
    fun provideTestA() : TestA = TestAImplementation()

    private var testCProvider: TestC?= null

    @Provides
    @Singleton
    fun provideTestC(testComponent: TestComponent) : TestC {
        if(testCProvider != null) return testCProvider as TestC

        val provider = Class.forName("com.example.feature.services.TestCImplementation\$Provider").kotlin.objectInstance as TestC.Provider
        return provider
            .get(testComponent)
            .also { testCProvider = it }
    }
}

TestComponent.kt

@Singleton
@Component(modules = [TestModule::class])
interface TestComponent {
    fun inject(activity: MainActivity)

    fun provideTestA() : TestA
}

Dagger configuration for the feature module:

TestDependencyModule.kt

@Module
class TestDependencyModule {

    @Provides
    @TestScope
    fun provideTestB(): TestB = TestBImplementation()

    @Provides
    @TestScope
    fun provideTestC(testB: TestB): TestC = TestCImplementation(testB)
}

TestDependencyComponent.kt

@TestScope
@Component(
    modules = [TestDependencyModule::class],
    dependencies = [TestComponent::class]
)
interface TestDependencyComponent {
    fun inject(receiver: TestBroadcastReceiver)

    fun testC(): TestC
}

Expected result

The interfaces TestC and TestA are injected in the MainActivity

The interfaces TestB and TestA are injected in the TestBroadcastReceiver

As expected the instance of the TestA implementation is unique but for the implementation of the TestB is not that way. As TestC depends on TestB the one injected in TestC is different from the one injected in the TestBroadcastReceiver with the @TestScope annotation.

So running the example project that you can find here I get the following log output

Instances injected in the MainActivity

D/TestB: instance 40525431
D/TestC: instance 119319268
D/TestA: instance 60713805

Instances injected in the TestBroadcastReceiver

D/TestB: instance 219966227
D/TestA: instance 60713805

I would like to share the same instance of TestB in both modules.

Any suggestion? Thanks in advance!

Community
  • 1
  • 1
Bruno
  • 299
  • 1
  • 7
  • 23
  • Somewhat out of scope, but why don't you use @ContributesAndroidInjector? – Yavor Mitev Nov 08 '19 at 13:23
  • @YavorMitev can you elaborate a bit more please? – Bruno Nov 08 '19 at 13:42
  • When I implement Dagger I mostly use as an example the code from https://github.com/google/iosched . Here is an article about it. https://medium.com/androiddevelopers/google-i-o-2018-app-architecture-and-testing-f546e37fc7eb . If you check the project, and mine is also like this, there are almost no Provides methods. You don't have to manually inject any from the Android classes. No need to create the Component, find reference for it and inject from Activity or Fragment manually. And like I said: Almost anything is done with @Inject constructor(). – Yavor Mitev Nov 08 '19 at 13:50
  • Here is an example class: https://github.com/google/iosched/blob/master/mobile/src/main/java/com/google/samples/apps/iosched/di/ActivityBindingModule.kt . But to be honest I have no experience with multimodule project so here I can't be much of a help. But I just constantly see here people writting too much code which is useless. I do almost anything with @Inject in my project. – Yavor Mitev Nov 08 '19 at 13:52
  • @YavorMitev thank you! :) I will have a look, but maybe I will have the same problem, not sure. The thing is that we also need to inject classes in different classes that are not Android classes so maybe we have to combine both. – Bruno Nov 08 '19 at 14:39
  • If the classes are not Android classes it is the easiest. Just @Inject on the constructor and eveything will work out of the box. Yes, when you have different implementation then Provieds is needed, but otherwise no complex logic. And also their projec is multi module so maybe you can "steal" something from them that will work for you. But for sure if you write less code it will be easier to figure out how to solve the problem. Good luck! – Yavor Mitev Nov 08 '19 at 15:02

2 Answers2

0

TestDependencyComponent can not access TestC from the component dependency on TestComponent because TestComponent does not expose TestC on its public API. If you add fun testC(): TestC on TestComponent I expect you will then get a duplicate binding exception when processing TestDependencyComponent. From there you will need to determine the right way to provide the TestC instance.

TrevJonez
  • 949
  • 8
  • 12
0

I was building two instances of the DaggerTestDependencyComponent one in the Injector and another different one when you are implementing TestC

The solution that I found was the following:

  • Create an object where I can instantiate the TestDependencyComponent that will be shared with the Injector and the TestCImplementation

    object FeatureInjector {
    
        val testDependencyComponent: TestDependencyComponent by lazy {
            DaggerTestDependencyComponent.builder()
                .testComponent(com.example.daggertest.dagger.Injector.testComponent)
                .build()
        }
    }
    
  • Now I modified my feature Injector like that:

    object Injector {
    
        lateinit var testDependencyComponent: TestDependencyComponent
    
        @JvmStatic
        internal fun getTestDependencyComponent(): TestDependencyComponent {
            if (!::testDependencyComponent.isInitialized) {
                testDependencyComponent = FeatureInjector.testDependencyComponent
            }
            return testDependencyComponent
        }
    }
    
  • And the TestCImplementation as follow:

    class TestCImplementation @Inject constructor(
        private val testB: TestB
    ) : TestC {
        override fun testCFun() {
            testB.testBFun()
            Log.d("TestC", "instance ${System.identityHashCode(this)}")
        }
    
        companion object Provider : TestC.Provider {
            override fun get(testComponent: TestComponent): TestC {
                return FeatureInjector.testDependencyComponent.testC()
            }
        }
    }
    

Running the code now I am getting the same instance of the TestB

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Bruno
  • 299
  • 1
  • 7
  • 23