TheHebrewHammer
TheHebrewHammer

Reputation: 3148

Dagger 2 can't inject from subcomponent

I know that generally it shouldn't make a difference that this is using Kotlin, but I've run into odd cases where the @Named qualifier needed a scope in Kotlin.

I have a ViewHolderFactory class that allows me to create a simple mapping of view type -> view holder class:

@Singleton
class ViewHolderFactoryImpl @Inject constructor(
        private val viewHolderComponentProvider: Provider<ViewHolderSubcomponent.Builder>
): ViewHolderFactory(mapOf(
        R.layout.view_error to ErrorViewHolder::class.java,
        R.layout.view_soft_error to SoftErrorViewHolder::class.java,
        R.layout.view_empty to EmptyViewHolder::class.java,
        R.layout.view_loading to LoadingViewHolder::class.java,
        R.layout.item_got_it to GotItViewHolder::class.java)) {

    override fun createViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val viewHolder = super.createViewHolder(parent, viewType)
        if (viewHolder is Injectable) {
            viewHolderComponentProvider.get()
                    .viewHolder(viewHolder)
                    .build()
                    .inject(viewHolder)
        }
        return viewHolder
    }
}

The ViewHolderSubcomponent is defined below, the goal is to be able to create one subcomponent for each view holder and inject a few things:

@ViewHolderScope
@Subcomponent(modules = [ViewHolderModule::class])
interface ViewHolderSubcomponent {

    fun inject(viewHolder: RecyclerView.ViewHolder)

    fun viewHolder(): RecyclerView.ViewHolder

    @Subcomponent.Builder
    interface Builder {
        @BindsInstance
        fun viewHolder(viewHolder: RecyclerView.ViewHolder): Builder
        fun build(): ViewHolderSubcomponent
    }
}

The ViewHolderModule is defined as:

@Module
class ViewHolderModule {
    @Provides @ViewHolderScope
    fun provideSectionTitleViewHolder(viewHolder: RecyclerView.ViewHolder): SectionTitleViewHolder =
            SectionTitleViewHolder(viewHolder.itemView)
}

When I run the app I find that the injection didn't work, my @Inject lateinit var values are null. Looking at the generated code I can see why:

@Override
public void inject(RecyclerView.ViewHolder viewHolder) {
  MembersInjectors.<RecyclerView.ViewHolder>noOp().injectMembers(viewHolder);
}

There's no MembersInjectors<RecyclerView.ViewHolder> created for this subcomponent. I can't figure out how to get this to work. I know that I should be able to inject into objects not created by dagger, I just can't quite figure out what I'm missing here.

Oh, if it helps any, I did make sure to include the ViewHolderSubcomponent in my AppModule's list of subcomponents

Upvotes: 4

Views: 654

Answers (2)

David Medenjak
David Medenjak

Reputation: 34542

inject(viewHolder: RecyclerView.ViewHolder) will always be a no-op, because framework classes (or most libraries in this case) don't have any @Inject annotated fields. Dagger will only generate code for the class named in your inject(MyClass instance) methods, not for any of its sub-types.

So if you have a ErrorViewHolder : RecyclerView.ViewHolder, then you have to use a component that has a inject(ErrorViewHolder instance) method to generate code to inject ErrorViewHolder.
To clarify, because it's generated code—and not dynamic reflection at runtime—calling inject(viewHolder: RecyclerView.ViewHolder) like you do with an viewHolder : ErrorViewHolder will still try to inject fields for RecyclerView.ViewHolder, not ErrorViewHolder. And RecyclerView.ViewHolder will always be a no-op as mentioned.

You will have to modify your setup quite a bit so that you can provide a specific subcomponent that can inject a specific viewholder, you can't use one "generic" component for different types. You could create a base class between RecyclerView.ViewHolder and ErrorViewHolder, but then again, you could only inject fields declared (and @Inject annotated) in your base class, not the specific child.

Upvotes: 1

tynn
tynn

Reputation: 39873

The Kotlin proptery itself is not known to Dagger. What you are trying to achieve could be handled as a field or setter injection. Since a setter is generated with any property I usually go for @set:Inject.

@set:Inject lateinit var myVar: Type

Alternatively you can consider constructor injections. Like this you can define the properties as val and private.

Upvotes: 0

Related Questions