Skip to content

Instantly share code, notes, and snippets.

@rharter
Last active September 10, 2019 15:31
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save rharter/2171ec81f18f93e7e7ebcad8dfdd3012 to your computer and use it in GitHub Desktop.

Dagger/Android with Internal Dependencies

I’ve figured out how to do what I wanted, though without some generation I’m not actually sure it’s worth it. Here’s a bunch more code with the solution I’ve figured out, in case anyone knows how to auto-generate this stuff, or if there’s a better way to do this.

The Goal: Allow use of Dagger-android AndroidInjectors without needing to make all dependencies of the target public.

The Scenario: I have a modular app where each feature is a separate module (among others). I have always hated making the ViewModels/ViewStates/ViewActions and other internal parts of the feature public, but that’s required with the current Dagger-android subcomponent approach.

Here’s my module setup:

// app/src/main/java/.../AppComponent.kt
@Component(modules = [OnboardingModule::class, ...])
interface AppComponent {
 val activityInjector: DispatchingAndroidInjector<Activity>
 ...
}

// onboarding/src/main/java/.../OnboardingModule.kt
@Module abstract class OnboardingModule {

  @ContributesAndroidInjector(modules = [Internal::class])
  abstract fun contributesPagePickerActivity(): PagePickerActivity

  @Binds abstract fun otherPublicItem(item: MyPublicItem): PublicItem

  @Module internal abstract class Internal {
 	// This should be internal
    @Binds abstract fun bindPagePickerViewModel(impl: PagePickerViewModel):
        ViewModel
  }
}

The reason that dependencies of the generated subcomponent have to be public is that the subcomponent implementation uses the parent component directly to access the providers, meaning that the dependencies need to be provided by the parent component (which lives in the app module, in my case).

To solve this, the feature module can be updated to create a Component instead of a Subcomponent, providing the dependencies via the Component.Builder, and exposing only the items that should be made available publicly.

// Public dagger.Module that exposes public items that should be available to other modules
@Module(includes = [OnboardingModule.Providers::class])
abstract class OnboardingModule {

  // Expose publicly available items
  @Binds
  @ActivityScope
  abstract fun bindPagePickerIntentFactory(factory: PagePickerIntentFactory): IntentFactory<PagePickerActivity>

  @Module
  object Providers {

    // This is how we expose the automatically generated AndroidInjector for our Activities
    //  to the rest of the graph, so that the app level injector can take care of injection.
    @Provides
    @JvmStatic
    @IntoMap
    @ClassKey(PagePickerActivity::class)
    fun bindAndroidInjectorFactory(component: OnboardingComponent): AndroidInjector.Factory<*> =
      component.pagePickerActivitySubcomponentFactory()

    // Using dagger to inject and construct the component's factory makes this a lot easier.
    @Provides
    @JvmStatic
    fun provideOnboardingComponent(factory: OnboardingComponent.Factory): OnboardingComponent = factory.create()
  }

  @ActivityScope
  @Component(modules = [InternalOnboardingModule::class])
  interface OnboardingComponent {

    // The component has to explicitly make public whatever is internal to it's graph that should be available to
    //  the rest of the app (via the public module that contains this component).  The key here is that, while the
    //  items exposed here need to be `public`, **their dependencies don't**, so the Activity that this subcomponent
    //  creates needs to be public (true for all activities so Android can construct them), but it's ViewModel can
    //  be internal or otherwise hidden from other modules in the app.
    fun pagePickerActivitySubcomponentFactory():
        InternalOnboardingModule_ContributesPagePickerActivity.PagePickerActivitySubcomponent.Factory

    // All dependencies used within the Component but not provided by the declared modules, or
    //  sharing a scope annotation, need to be provided via a Factory.
    @Component.Factory
    internal interface DaggerFactory {
      fun create(
        @BindsInstance application: Application,
        @BindsInstance navigator: Navigator,
        @BindsInstance experimentStore: ExperimentStore,
        @BindsInstance config: Config
      ): OnboardingComponent
    }

    // This wrapper class allows the factory to easily be constructed by the containing Component without this
    //  module needing to have knowledge of it.
    class Factory @Inject constructor(
      private val application: Application,
      private val navigator: Navigator,
      private val experimentStore: ExperimentStore,
      private val config: Config
    ) {
      fun create(): OnboardingComponent = DaggerOnboardingModule_OnboardingComponent.factory()
        .create(application, navigator, experimentStore, config)
    }
  }
}

// Internal dagger.Module that's used to assemble a graph that relies on components that aren't visible
//  to the rest of the app (i.e. the `PagePickerViewModel`, in this case).  Dagger-android created Injectors
//  must be included here since their autogenerated Subcomponents are created within the Component in which
//  they're contained, meaning that **all of their dependencies need to be visible to the containing
//  Component**.
@Module
internal abstract class InternalOnboardingModule {

  // This annotation will result in a component being generated like above.  Dependencies supplied by the
  //  declared modules, or sharing the scope annotation with this item, are considered internal, all others
  //  are added to the generated component's Factory.
  @ActivityScope
  @ContributesAndroidInjector(modules = [Providers::class, Internal::class])
  abstract fun contributesPagePickerActivity(): PagePickerActivity
  
  // Dependencies from this module that should be publicly available in the containing Component
  //  get declared here.
  @Binds
  @ActivityScope
  abstract fun bindPagePickerIntentFactory(factory: PagePickerIntentFactory): IntentFactory<PagePickerActivity>

  @Module
  internal abstract class Internal {

    // `PagePickerViewModel` has an `@Inject` annotated constructor, but also has an `@ActivityScope`
    //  annotation to match the `contributesPagePickerActivity` above, so it'll be supplied within the
    //  context of the generated Component and doesn't need to be public to consumers.
    @Binds
    @IntoMap
    @OnboardingViewModelFactory
    @ViewModelKey(PagePickerViewModel::class)
    abstract fun bindPagePickerViewModel(impl: PagePickerViewModel): ViewModel
  }

  @Module
  internal object Providers {

    @Provides
    @JvmStatic
    @OnboardingViewModelFactory
    fun providesViewModelFactory(
      @OnboardingViewModelFactory providers: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
    ): ViewModelProvider.Factory = PigmentViewModelFactory(providers)
  }
}

I’m not sure if there’s enough information to know at generation time which elements are publicly available or not, but it may be possible to look at the containing class and modules: If the @ContributesAndroidInjector annotation exists within a non-public container, then generate a Component who’s builder takes in all dependencies not provided by the containing module, or modules included in the annotation’s modules list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment