Search code examples
androidkotlindagger-2dagger

Dagger-android share dependencies between Activities


I'm using dagger-android for DI. There're a few common dependencies I want to provide to every Activity. They depend on an Activity class but don't relay on a certain Activity implementation. Like this:

class NavigatorImpl(private val activity: Activity) : Navigator {..}

Now I have to write the same code at every ActivityModule:

@Module
class SomeActivityModule {

    @Provides
    fun navigator(activity: SomeActivity): Navigator = NavigatorImpl(activity)

    // some deps...

}

@Module
class AnotherActivityModule {

    @Provides
    fun navigator(activity: AnotherActivity): Navigator = NavigatorImpl(activity)

    // another deps...

}

I tried to provide these dependencies via AndroidSupportInjectionModule but it turns out that Dagger generates separate Submodules for each Activity, and we can access to an Activity dependency only at the certain modules (SomeActivityModule and AnotherActivityModule at this example):

@Module(includes = [AndroidSupportInjectionModule::class])
interface AppInjectionModule {

    @PerActivity
    @ContributesAndroidInjector(modules = [SomeActivityModule::class])
    fun someActivity(): SomeActivity

    @PerActivity
    @ContributesAndroidInjector(modules = [AnotherActivityModule::class])
    fun anotherActivity(): AnotherActivity

    // we don't have any Activity in the graph -> throws an error
    @PerActivity
    @Provides
    fun navigator(navigator: NavigatorImpl): Navigator

}

So how can I provide such dependencies without duplicating code in every ActivityModule?


Solution

  • First of all you put all of your common things into a module that you can reuse. (Also please use constructor injection with @Binds as shown below

    class Navigator @Inject constructor(activity : Activity) { /* ... */ }
    
    @Module
    interface BaseActivityModule {
    
        @Binds
        fun navigator(navigator: NavigatorImpl): Navigator
    
        // other bindings...
    
    }
    

    Then you have to switch to custom subcomponent declarations instead of @ContributesAndroidInjector. There is a related open issue, but for now you have to write the boilerplate.

    You declare an AndroidInjector.Factory that binds everything you need, then you use this for your subcomponent builder instead of the default AndroidInjector.Builder.

    object MyCustomInjector {
    
      abstract class ActivityBuilder<T : Activity> : AndroidInjector.Factory<T> {
        override fun create(instance: T): AndroidInjector<T> {
          seedInstance(instance)
          activity(instance)
          return build()
        }
    
        @BindsInstance
        abstract fun seedInstance(instance: T)
    
        @BindsInstance
        abstract fun activity(instance: Activity)
    
        abstract fun build(): AndroidInjector<T>
      }
    }
    
    @Module(subcomponents = [(SomeActivityModule.SomeActivitySubcomponent::class)])
    abstract class SomeActivityModule {
    
      @Binds
      @IntoMap
      @ActivityKey(SomeActivity::class)
      internal abstract fun contributeSomeActivity(builder: SomeActivitySubcomponent.Builder): AndroidInjector.Factory<out Activity>
    
      @PerActivity
      @Subcomponent(modules = [BaseActivityModule::class])
      interface SomeActivityActivitySubcomponent : AndroidInjector<SomeActivity> {
        @Subcomponent.Builder
        abstract class Builder : MyCustomInjector.ActivityBuilder<SomeActivity>()
      }
    }
    

    Your subcomponent builder thus binds both SomeActivity and Activity, and your subcomponent includes the BaseActivityModule with the bindings defined above.

    For more information you can also have a look at this related question with a few more steps...