Search code examples
androiddependency-injectiondagger-2dagger

Dagger2: How no to duplicate Module-Component forActivity / Fragments, relying on single Base(Module/Component) for Base(Activity/Fragment) &


Given BaseFragment and its subclasses: DerivedFragmentA, DerivedFragmentB, ...

Let's say, that most of @Inject fields are common for each fragment and hence declared in BaseFragment:

abstract class BaseFragment : DaggerFragment() {
    @Inject lateinit var vmFactory: ViewModelProvider.Factory
}

class DerivedFragmentA : BaseFragment()
class DerivedFragmentB : BaseFragment()
...

For each of derived fragments we have to manually create Module-Component pairs, such as:

@Subcomponent
interface DerivedFragmentAComponent : AndroidInjector<DerivedFragmentA> {

    @Subcomponent.Builder
    abstract class Builder : AndroidInjector.Builder<DerivedFragmentA>()
}

@Module(subcomponents = [DerivedFragmentAComponent::class])
abstract class DerivedFragmentAModule {

    @Binds @IntoMap @FragmentKey(DerivedFragmentA::class)
    abstract fun bind(builder: DerivedFragmentAComponent.Builder): AndroidInjector.Factory<out Fragment>
}

And install each of them to some outer component, like that:

@Subcomponent(modules = [DerivedFragmentAModule::class, DerivedFragmentBModule::class, ...])
interface MainComponent : AndroidInjector<MainActivity> {

    @Subcomponent.Builder
    abstract class Builder : AndroidInjector.Builder<MainActivity>()
}

But this is kind of boilerplate.

If we try to make just a single Module-Component for BaseFragment and install only it to the MainComponent - we will get a runtime exception during invocation of AndroidInjector.inject(fragment) method, with the following message:

  "No injector factory bound for Class<DerivedFragmentA>. Injector factories were bound for supertypes of DerivedFragmentA: BaseFragment. Did you mean to bind an injector factory for the subtype?"

Is there any way to fix that and avoid code duplication? Or as Dagger-2 strongly relies on classnames, its impossible ?


Solution

  • Injection with Dagger 2 always works with the type you specify. inject(fragment : BaseFragment) will only ever inject the fields of BaseFragment and none of the fields declared in any subclasses. That's just something you have to keep in mind.

    You say you would like to just declare one component and inject things into the BaseFragment only, so that's exactly what you can do. Instead of creating a subcomponent for your DerivedFragment you create one for your BaseFragment...

    @Subcomponent
    interface BaseFragmentComponent : AndroidInjector<BaseFragment> {
    
        @Subcomponent.Builder
        abstract class Builder : AndroidInjector.Builder<BaseFragment>()
    }
    

    Then you can bind the BaseFragment.Builder to your DerivedFragmentXs.

    @Module(subcomponents = [BaseFragmentComponent::class])
    abstract class BaseFragmentModule {
    
        @Binds @IntoMap @FragmentKey(DerivedFragmentA::class)
        abstract fun bind(builder: BaseFragmentComponent.Builder): AndroidInjector.Factory<out Fragment>
    
        @Binds @IntoMap @FragmentKey(DerivedFragmentB::class)
        abstract fun bind(builder: BaseFragmentComponent.Builder): AndroidInjector.Factory<out Fragment>
    
        @Binds @IntoMap @FragmentKey(DerivedFragmentC::class)
        abstract fun bind(builder: BaseFragmentComponent.Builder): AndroidInjector.Factory<out Fragment>
    }
    

    The important bit is to set @FragmentKey(DerivedFragmentA::class) to reference the subclass, since that's the one Dagger will look up when calling AndroidInjection.inject(fragment).


    I'd still recommend you not to use this approach, as you'd end up with a mix of some fragments being fully injected and others where it's just the BaseFragment. This sounds confusing to me.

    You could just use @ContributesAndroidInjector instead to generate the boilerplate code for you and properly inject every fragment.