Search code examples
androiddagger-2dagger

No injector factory bound with @ContributesAndroidInjector


Please don't mark this duplicate, I've read all the other answers about this issue. I'm not asking what the issue means, I'm asking why this particular code produces this error.

I'm trying to make an Activity component/injector that uses SessionComponent as its parent:

AppComponent:

@Singleton
@Component(modules = [AppModule::class, AndroidSupportInjectionModule::class])
interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(ltiApp: LTIApp): Builder
        fun build(): AppComponent
    }

SessionComponent:

@SessionScope
@Component(
        dependencies = [AppComponent::class],
        modules = [SessionModule::class, CommentaryModule::class, EducationCenterModule::class])
interface SessionComponent {

EducationCenterModule

@dagger.Module
abstract class EducationCenterModule {
    @EducationScope
    @ContributesAndroidInjector()
    abstract fun educationCenterActivity(): EducationCenterActivity
}

How come I get an error for injector factory even though I have @ContributesAndroidInjector inside a Module?

Caused by: java.lang.IllegalArgumentException: No injector factory bound for Class


Solution

  • If possible, move your @ContributesAndroidInjector into your AppModule, which will likely involve some refactoring between AppComponent and SessionComponent.


    dagger.android injects Activity instances by calling getApplication() on the Activity, casting that to a HasActivityInjector, and calling activityInjector().inject(activity) on it (code). In turn, apps that use DaggerApplication (or the code on the dagger.android how-to page) will inject a DispatchingAndroidInjector<Activity>, which injects a Map<Class, AndroidInjector.Builder> that is built using multibindings. Though it's possible to inject into this map directly, you may also use @ContributesAndroidInjector (as you have done here) as a shortcut that produces the multibinding and subcomponent.

    Though you have @ContributesAndroidInjector bound inside a @Module, you have two top-level components: AppComponent and SessionComponent. dagger.android is not prepared for this: Your Application likely uses AppComponent for its injection, and because you haven't installed EducationCenterModule into AppComponent, the multibound map will not contain the binding that your @ContributesAndroidInjector method installs.

    This probably requires some refactoring, but for important reasons: Through intents, back stacks, and activity management, Android reserves the right to recreate your Activity instance whenever it wants to. Though your Application subclass likely guarantees that an AppComponent will exist by then (by creating and storing that component within onCreate), there is no such guarantee that your SessionComponent will exist, nor any established way for an Android-created Activity instance to find the SessionComponent that it can use.


    The most common way to solve this problem is to separate the Android lifecycle from your business logic, such that dagger.android manages your Android components on their own lifecycle, and those Android components create/destroy SessionComponent and other business logic classes as needed. This may also be important if you ever require Service, ContentProvider, or BroadcastReceiver classes, as those will definitely only have access to the application, and may restore or create sessions of their own. Finally, this also means that a Session will necessarily last longer than an Activity instance, which might mean that your Session will not be garbage collected until Android destroys your Activity, and may also mean that you have multiple concurrent SessionComponent instances.

    Edit/elaboration: First and foremost you'll need to decide whether sessions outlive Activities, Activities outlive Sessions, or neither. I bet it's "neither", which is fine: at that point I'd write an injectable @Singleton SessionManager that goes inside AppComponent and manages the creation, recreation, and fetching of SessionComponent. I'd then try to divide it so most of the business logic is on the SessionComponent side, and by calling sessionManager.get().getBusinessObject() you can access it from SessionComponent. This also works well to keep you honest, since the business logic side might be easy to unit test using Robolectric or Java, while the Android side might require instrumentation tests in an emulator. And, of course, you can use @Provides methods in your AppComponent modules to pull out your SessionComponent, BusinessObject, or any other relevant instance out of the SessionManager side.

    However, if you are 100% sure that you want SessionComponent to be the container for your Activity, and that you don't mind managing the session creation and deletion in your Application subclass. If that's the case, then rather than using DaggerApplication, you can write your Application subclass to implement HasActivityInjector (etc), and delegate the activityInjector() method to a class on a SessionComponent instance that you create. This means that AppComponent would no longer include AndroidSupportInjectionModule, because you no longer inject DispatchingAndroidInjector or its Map. However, this is an unusual structure with implications for your application, so you should consider your component structure carefully before proceeding and document it heavily if you choose the non-standard route.