How to avoid binding FragmentX to Fragment

2019-08-05 11:26发布

问题:

How to avoid binding FragmentX to Fragment

I am having several files where I just declare the binding of a FragmentX to a Fragment (or ActivityX to Activity) in order to be able to inject the objects as base class dependencies.

Those files look like this

@Module
abstract class FragmentXModule {

    @Binds
    @FragmentScoped
    internal abstract fun bindFragment(fragmentX: FragmentX): Fragment
}

This is repeating over and over again.

Is it possible to avoid those file creation repeat and group all bindings in one file?

回答1:

Update: It's actually much easier!

I wrote quite a long answer on how it's not really possible without duplicating all the code, when in fact this is not the case. You can read the old answer below for reference, I will just include the simple solution here on top.

Turns out there is a good reason why there is an AndroidInjector.Factory interface and a AndroidInjector.Builder class. We can just implement the interface ourselves and use our builder instead! This way we can still keep using the Dagger Android parts to inject our components, with no need to create things from scratch ourselves.

Different components can use different builders, in the end they just have to implement AndroidInjector.Factory<T>. The following builder shows a generic approach to bind the type and one supertype.

abstract class SuperBindingAndroidInjectorBuilder<S, T : S> : AndroidInjector.Factory<T> {

    override fun create(instance: T): AndroidInjector<T> {
        seedInstance(instance)
        superInstance(instance)
        return build()
    }

    // bind the object the same way `AndroidInjector.Builder` does
    @BindsInstance
    abstract fun seedInstance(instance: T)

    // _additionally_ bind a super class!
    @BindsInstance
    abstract fun superInstance(instance: S)

    abstract fun build(): AndroidInjector<T>
}

We can use this Builder instead of AndroidInjector.Builder which allows us to bind a supertype as well.

@Subcomponent
interface MainActivitySubcomponent : AndroidInjector<MainActivity> {

    @Subcomponent.Builder
    abstract class Builder : SuperBindingAndroidInjectorBuilder<Activity, MainActivity>()

}

With the builder above we can declare the base type that we wan't to inject as our first type parameter along with the actual type that we are going to inject. This way we can provide both, Activity and MainActivity with minimal effort.


Is it possible to avoid those file creation repeat and group all bindings in one file?

There are basically just 2 ways to add a binding to Dagger. One is the module approach that you took, which requires to add a module with the correct binding, the other is to bind the instance directly to the Component.Builder. (Yes, you could also add a module with a constructor argument to the builder, but this has the same effect and leads to even more code)

If you're not using AndroidInjection but are still manually creating every component, then all you have to do is add a @BindsInstance abstract fun activity(instance: Activity) to your Subcomponent.Builder and pass it in while constructing the component. If you want to make use of AndroidInjection then we have to do a bit more, which I will detail in the following post.

In your specific use case I would just keep doing what you're doing now, but I will show another way how you could handle this. The downside here is that we can't use @ContributesAndroidInjector or AndroidInjection.inject() anymore...

Why we can't use @ContributesAndroidInjector

@ContributesAndroidInjector will generate the annoying-to-write boilerplate for us, but we need to modify this generated code. In particular, we need to use different interfaces that our components implement and the only option to do so is to write the boilerplate ourselves. And yes, of course we could create our own AnnotationProcessor that generates Boilerplate like we want it, but this is out of the scope of this answer.

The following part is outdated, but I will leave it as reference to what actually goes on within AndroidInjection. Please use the solution presented above if you want to add additional bindings.

The AndroidInjection already binds SpecificActivity itself to the graph, but it will not allow us to treat it as an Activity. To do this, we will have to use our own classes and also bind it as an Activity. Maybe Dagger will get a feature like this in the future.

What modifications?

We start with our default setup which @ContributesAndroidInjector would generate for us. This is the one you should be familiar with. (If you're not using AndroidInjection yet, don't worry, we'll be creating our own setup in the next step)

@Component(modules = [AndroidSupportInjectionModule::class, ActivityModule::class])
interface AppComponent {

    fun inject(app: App)

    @Component.Builder
    interface Builder {
        @BindsInstance fun app(app: App) : AppComponent.Builder
        fun build(): AppComponent
    }
}

@Module(subcomponents = [MainActivitySubcomponent::class])
internal abstract class ActivityModule {
    @Binds
    @IntoMap
    @ActivityKey(MainActivity::class)
    internal abstract fun bindMainActivityFactory(builder: MainActivitySubcomponent.Builder): AndroidInjector.Factory<out Activity>
}

@Subcomponent
interface MainActivitySubcomponent : AndroidInjector<MainActivity> {

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

}

With this setup we can now safely inject MainActivity, even bind it to Activity in a module, but that's not what we want. We want this binding to be automated. Let's see if we can do better.

As previously hinted, we can't use AndroidInjection.inject(). Instead, we need to create our own interfaces. In the spirit of so many Android libraries I will call my interface AwesomeActivityInjector. I will keep this short and simple, but you can read AndroidInjector for more information—which I basically just copied.

I added one modification though. activity(activity : Activity) will allow us to bind our activity to the component as well.

interface AwesomeActivityInjector<T : Activity> {

    fun inject(instance: T)

    interface Factory<T : Activity> {
        fun create(instance: T): AwesomeActivityInjector<T>
    }

    abstract class Builder<T : Activity> : AwesomeActivityInjector.Factory<T> {
        override fun create(instance: T): AwesomeActivityInjector<T> {
            activity(instance) // bind the activity as well
            seedInstance(instance)
            return build()
        }

        @BindsInstance
        abstract fun seedInstance(instance: T)

        @BindsInstance
        abstract fun activity(instance: Activity)

        abstract fun build(): AwesomeActivityInjector<T>
    }
}

This is a simple interface that our component and its builder will implement, in the very same way as AndroidInjection does it currently. By using a common interface on our subcomponents we can use it to create our components and inject our activities.

Adapting our components

Now that we have our interface, we switch out our Subcomponent and Module to use that instead. This is still the same code, I just replaced AndroidInjector with AwesomeActivityInjector.

@Module(subcomponents = [MainActivitySubcomponent::class])
internal abstract class ActivityModule {
    @Binds
    @IntoMap
    @ActivityKey(MainActivity::class)
    internal abstract fun bindMainActivityFactory(builder: MainActivitySubcomponent.Builder): AwesomeActivityInjector.Factory<out Activity>
}

@Subcomponent
interface MainActivitySubcomponent : AwesomeActivityInjector<MainActivity> {

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

}

Preparing for injection

Now with everything set up, all we need to do is add some code to inject our Activity. The AndroidInjection part does this more nicely by having the application implement an interface, etc. You can look up how this is done, but for now I will just directly inject our factories and use them.

Be careful with Dagger and Kotlin when using Wildcards!

class App : Application() {

    @Inject
    lateinit var awesomeInjectors: Map<Class<out Activity>, @JvmSuppressWildcards Provider<AwesomeActivityInjector.Factory<out Activity>>>
}

object AwesomeInjector {

    fun inject(activity: Activity) {
        val application = activity.application as App

        val factoryProviders = application.awesomeInjectors
        val provider = factoryProviders[activity.javaClass] as Provider<AwesomeActivityInjector.Factory<out Activity>>

        @Suppress("UNCHECKED_CAST")
        val factory = provider.get() as AwesomeActivityInjector.Factory<Activity>
        factory.create(activity).inject(activity)
    }
}

Using our AwesomeActivityInjector

Now with all of this we got ourselves a working injection. We can now inject both, Activity as well as MainActivity.

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var mainActivity: MainActivity
    @Inject
    lateinit var activity: Activity

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        AwesomeInjector.inject(this)
    }
}

This code works for Activities and can be similarly expanded to also cover Fragments.

Final Words

Yes, this is probably overkill. At least I would say so if we just want to bind MainActivity as an Activity. I wrote this answer to give an example of how AndroidInjection works and how one could adapt and modify it.

With a similar approach you can also have PerScreen scopes that survive orientation changes, or a UserScope that lives shorter than the Application, but longer than an Activity. None of this will work currently out of the box with AndroidInjection and requires custom code like shown above.