11

I'm trying to figure out a less-boilerplate-y way to implement an ActivityModule that is used in all of my app activities. This is my current setup:

ActivityModule:

@Module
class ActivityModule(private val activity: Activity) {

    @Provides @ActivityScope
    fun providesActivity(): Activity = activity

    @Provides @ActivityContext @ActivityScope
    fun providesContext(): Context = activity

    @Provides @ActivityContext @ActivityScope
    fun providesLayoutInflater(): LayoutInflater = activity.layoutInflater

    @Provides @ActivityContext @ActivityScope
    fun providesResources(): Resources = activity.resources

}

AppActivityModule(provides activities for AndroidInjectionModule)

@Module(subcomponents = [
        AppActivityModule.WelcomeActivityComponent::class
    ])
    internal abstract class AppActivityModule {

        @Binds 
        @IntoMap 
        @ActivityKey(WelcomeActivity::class)
        abstract fun bindWelcomeActivityInjectorFactory(builder: WelcomeActivityComponent.Builder): AndroidInjector.Factory<out Activity>

        @ActivityScope
        @Subcomponent(modules = [(ActivityModule::class)])
        interface WelcomeActivityComponent : AndroidInjector<WelcomeActivity> {
        @Subcomponent.Builder abstract class Builder : AndroidInjector.Builder<WelcomeActivity>() {
            abstract fun activityModule(myActivityModule: ActivityModule): AndroidInjector.Builder<WelcomeActivity>

            override fun seedInstance(instance: WelcomeActivity) {
                activityModule(ActivityModule(instance))
            }
        }
    }
}

What I want AppActivityModule to be instead is:

@Module
internal abstract class AppActivityModule {
    @ContributesAndroidInjector(modules = [(ActivityModule::class)])
    abstract fun contributeWelcomeActivityInjector(): WelcomeActivity
}

But this, quite understandbly, gives me an error /di/AppActivityModule_ContributeWelcomeActivityInjector.java:29: error: @Subcomponent.Builder is missing setters for required modules or subcomponents: [...di.modules.ActivityModule]

My question is - is there a less boilerplate-y way to achieve what I'm trying to do? I know about @Bind and @BindsInstance (from this answer) but this seems to only work if I have a module-per activity and bind the concrete activity type which I don't want in this case - I want ActivityModule to work with all activities.

Valentin
  • 1,731
  • 2
  • 19
  • 29

1 Answers1

6

One way to minimize the boilerplate is to make a generic ActivityModule and then create a small specific Module per Activity.

// Abstract class so you don't have to provide an instance
@Module
abstract class ActivityModule {

    // No need for ActivityScope: You're always binding to the same Activity, so
    // there's no reason to have Dagger save your Context instance in a Provider.
    @Binds @ActivityContext
    abstract fun providesContext(activity: Activity): Context

    // This doesn't *have* to be in a companion object, but that way
    // Android can do a static dispatch instead of a virtual method dispatch.
    // If you don't need that, just skip the constructor arguments and make these
    // normal methods and you'll be good to go.
    companion object {
        @Provides @ActivityContext
        fun providesLayoutInflater(activity: Activity): LayoutInflater = 
            activity.layoutInflater

        @Provides @ActivityContext
        fun providesResources(activity: Activity): Resources = activity.resources
    }
}

(In Dagger versions earlier than 2.26, you may need to add @Module on your companion object and @JvmStatic on your @Provides methods as in this previous revision. Since Dagger 2.26 in in January 2020, those are not necessary. Thanks arekolek!)

And your module:

@Module
internal abstract class AppActivityModule {

    @Module
    internal interface WelcomeActivityModule {
      // The component that @ContributesAndroidInjector generates will bind
      // your WelcomeActivity, but not your Activity. So just connect the two,
      // and suddenly you'll have access via injections of Activity.
      @Binds fun bindWelcomeActivity(activity: WelcomeActivity) : Activity
    }

    @ContributesAndroidInjector(
        modules = [ActivityModule::class, WelcomeActivityModule::class])
    abstract fun contributeWelcomeActivityInjector(): WelcomeActivity
}

Note that though this works for Activity, Service, BroadcastReceiver, and others, you might not want to be so quick about it for Fragment. This is because dagger.android handles fragment hierarchies with parent fragments, so from within a child component you might have access to YourApplication, YourActivity, YourParentFragment, and YourChildFragment, and all of their components. If something in YourChildFragmentComponent depends on an unqualified Fragment, it would be ambiguous whether it really wants YourParentFragment or YourChildFragment. That said, this design does make sense for Activities and certain Fragments, so it makes sense to use it (cautiously).


EDIT: What is @ActivityContext doing here?

@ActivityContext here is a qualifier annotation you'd define, which you can use to distinguish bindings of the same type in Dagger and other DI framework, presumably @ApplicationContext Context vs @ActivityContext Context. It would work to leave it out to try it, but I heavily recommend keeping it and avoiding binding an unqualified Context: Application and Activity contexts may be different, particularly in multi-screen or auto environments, and to get the right resources and data you should be precise about which you use. You can use this one as an example.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • companion object is still an object, so what you say about "static dispatch instead of a virtual method dispatch" is not true. @JvmStatic creates a static method, that calls an instance method on the companion object. There's no way to use static provides in a Kotlin-only codebase, as far as I know. Either you have to make the module in Java, or the component in Java. – arekolek Feb 08 '18 at 12:01
  • 2
    @arekolek You're right that the companion object is implemented in Kotlin as a Singleton, and that an instance call is necessary in Kotlin. (I hadn't know that.) On the other hand, Kotlin handles that `static` indirection transparently, while Dagger will see a `@Provides` instance method and assume that _each Component instance_ needs to keep an instance of the Module regardless of what Kotlin does behind the scenes. Also, if Kotlin's generated instance method is `final`, it will _still_ translate into static dispatch even if it's on an instance method. (I'd need to dig in to the bytecode.) – Jeff Bowman Feb 08 '18 at 18:36
  • Hmm, it makes sense that a `static` method calling a `final` method on an object kept in a `static final` field could be optimized by the compiler, although this is the first time I hear about it. Methods are `final` by default in Kotlin and they can't be made `open` in `object`s, I checked the bytecode and it's `final` indeed. Can you point me to some documentation that says no virtual method call happens in this scenario? – arekolek Feb 10 '18 at 04:10
  • 1
    By the way, it seems that you have to annotate the `companion object` with `@Module`, just to make dagger-compiler happy. Also, `providesContext` should be declared abstract, to make Kotlin happy. – arekolek Feb 10 '18 at 04:21
  • 1
    @arekolek Because the method call goes to a `final` method, there is no other possible implementation, so [JLS 8.4.3.3](https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.3.3) allows that "at run time, a machine-code generator or optimizer can 'inline' the body of a `final` method, replacing an invocation of the method with the code in its body." To be fair, [this SO answer](https://stackoverflow.com/q/4279420/1426891) rebuts that, saying that the Hotspot VM will optimize pre-emptively and undo if an override is found. I don't know offhand what Dalvik and ART do. – Jeff Bowman Feb 10 '18 at 04:21
  • what is @ActivityContext here? – M Rajoy Oct 17 '19 at 11:25
  • @JeffBowman thanks, however it seems to me you are assigning this qualifier to an object of type `Context` and then using it to reference `Activity` objects in your companion object, did I miss something here? – M Rajoy Oct 18 '19 at 08:43
  • @GabrielSanmartin Each use of a qualifier is independent; there's no "assignment". It's more difficult to read in Kotlin, because the return values come at the end of the line instead of the beginning, but the point is that the top `@Binds` method returns the Activity whenever you inject an `@ActivityContext Context`, not a bare `Context` and not an `@ApplicationContext`. Similarly for LayoutInflator etc: Those bindings are only accessible if you inject `@ActivityContext LayoutInflator` etc. The qualifier is part of the key. See [the docs](https://dagger.dev/users-guide.html#qualifiers). – Jeff Bowman Oct 18 '19 at 13:26
  • 1
    I edited the answer because since [Dagger 2.26](https://github.com/google/dagger/releases/tag/dagger-2.26) we don't need `@Module` on `companion object` and `@JvmStatic` on the functions in it – arekolek Jun 18 '20 at 16:30
  • 1
    @arekolek Thank you! I appreciate your edit. I'll add a note to the text linking to the previous version, just in case any future readers are stuck on an older version of Dagger, but I'll leave the code snippet with your changes. – Jeff Bowman Jun 18 '20 at 17:27