Search code examples
androiddagger-2daggerdagger-hilt

Hilt - inject classes as interfaces


Lets say I have two interfaces and two classes that implements them.

interface ITest1 {
    fun doSomething()
}

interface ITest2 {
    fun doSomethingElse()
}

class Test1 @Inject constructor(): ITest1 {

    override fun doSomething() {
        println("Something")
    }
}
    
class Test2 @Inject constructor(): ITest2 {

    @Inject
    lateinit var test1: ITest1

    override fun doSomethingElse() {
        test1.doSomething()
        println("Something else ")
    }
}

I can make this work if I use @Bind:

@Module
@InstallIn(SingletonComponent::class)
internal abstract class DependenciesBindings {
    @Singleton
    @Binds
    abstract fun bindTest1(test1: Test1): ITest1

    @Singleton
    @Binds
    abstract fun bindTest2(test2: Test2): ITest2
}

But I would like to do some configuration to classes before they are injected. To do so, I tried to use @Provide:

@Module(includes = [DependenciesBindings::class])
@InstallIn(SingletonComponent::class)
object Dependencies {

    @Singleton
    @Provides
    fun provideTest1(): Test1 {
        return Test1()
    }

    @Singleton
    @Provides
    fun provideTest2(): Test2 {
        return Test2()
    }
}

@Module
@InstallIn(SingletonComponent::class)
internal abstract class DependenciesBindings {
    @Singleton
    @Binds
    abstract fun bindTest1(test1: Test1): ITest1

    @Singleton
    @Binds
    abstract fun bindTest2(test2: Test2): ITest2
}

But this crashes app:

Caused by: kotlin.UninitializedPropertyAccessException: lateinit property test1 has not been initialized at Test2.getTest1(Test2.kt:8) at Test2.doSomethingElse(Test2.kt:11)

Why this happens?

I would expect ITest1 to be correctly injected as Test1 is provided and then bind to ITest1 interface.

Is there a workaround to make this work?


Solution

  • By using a @Provides method, you are constructing the instance yourself rather than using Dagger. Consequently, Dagger will allow you to do so without calling your @Inject-annotated methods or populating your @Inject-annotated fields, such that getTest1 will fail on the uninitialized field.

    A more common pattern might have the bindings for ITest2 inject a Test2 instance (after all, @Provides methods can take arguments from the graph), which allows Dagger to create the Test2 instance and allows you to manipulate it before returning:

    @Module(/* no includes */)
    @InstallIn(SingletonComponent::class)
    object Dependencies {
    
        @Singleton
        @Provides
        fun provideTest1(test1: Test1): ITest1 {
            // manipulate test1
            return test1
        }
    
        @Singleton
        @Provides
        fun provideTest2(test2: Test2): ITest2 {
            // manipulate test2
            return test2
        }
    }
    

    If you cannot allow Dagger to create your Test1 or Test2 directly—for instance, if you don't have control over the constructor—you can inject a MembersInjector<Test1> and use that to inject the @Inject fields and methods.

    @Module(includes = [DependenciesBindings::class])
    @InstallIn(SingletonComponent::class)
    object Dependencies {
    
        @Singleton
        @Provides
        fun provideTest1(injector: MembersInjector<Test1>): Test1 {
            val test1 = Test1()
            injector.injectMembers(test1)
            return test1
        }
    
        @Singleton
        @Provides
        fun provideTest2(injector: MembersInjector<Test2>): Test2 {
            val test2 = Test2()
            injector.injectMembers(test2)
            return test2
        }
    }