Search code examples
androidkotlinscopedagger-2dagger

Dagger2 custom scopes: Are custom scopes not seen on @Inject constructor injections when using provider methods in a module?


The following is the code

Custom Annotation classes:

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CtxMain

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class MyScope

Component:

@MyScope
@Component(modules = [ToastMakerModule::class,ActionsModule::class])
interface ActionsComponent {
    fun injectIntoMain(activity: MainActivity)
}

Modules (Seperate File are made for each module):

@Module
class ActionsModule(private var context: Context) {
    @Provides
    @CtxMain
    fun providesContext():Context{
        return context
    }

    @Provides
    fun providesSharedPrefs(@CtxMain context: Context): SharedPreferences{
        return context.getSharedPreferences("PreferencesFile",Context.MODE_PRIVATE)
    }
}

@Module
class ToastMakerModule {
    @Provides
    fun provideToastMaker(@CtxMain context: Context): ToastMaker{
        return ToastMaker(context)
    }
}

Models:

@MyScope
class ToastMaker @Inject constructor(@CtxMain private val context:Context) {
    fun showToast(){
        Toast.makeText(context,"This is a Toast",Toast.LENGTH_SHORT).show()
    }
}

MainActivity:

class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var toastMaker: ToastMaker

    @Inject
    lateinit var toastMaker2: ToastMaker

    @Inject
    lateinit var sharedPreferences: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        DaggerActionsComponent.builder()
            .actionsModule(ActionsModule(this))
            .build()
            .injectIntoMain(this)

        toastMaker.showToast()
    }
}

Upon running this code the expexted ouptut was that due to @MyScope annotation toastMaker and toastMaker2 should point to the same object but they are not even tough the ToastMaker class has been annotated with @MyScope but still different obejcts are created for each (different hashcodes), but upon modifying the code (removing the provider for ToastMaker) i.e.

Modified ToastMakerModule (commented the provider)

@Module
class ToastMakerModule {
//    @Provides
//    fun provideToastMaker(@CtxMain context: Context): ToastMaker{
//        Log.d("#D","Provider de ToastMaker")
//        return ToastMaker(context)
//    }
}

Now when the Field Injection is coming from the ToastMaker Class itself, both toastMaker and toastMaker2 point to the same obejct (same hashcode), What am i missing here? why is provider method not returning me the same instance even tough its marked with @MyScope. i know we can mark the provider method with @MyScope annotation to make provider method return the same instance but shouldnt this work because the ToastMaker class is annotated with @MyScope. when provider method is removed its working fine, why?


Solution

  • Because you've offered Dagger a @Provides method, it will use that and completely ignore the @Inject-annotated constructor and any scopes you list on the class.

    As Dagger maintainer Brad Corso mentions in google/dagger#3361 "Singletons are initilized several times when injected into application" (emphasis mine):

    However, even if your actual SubsManager class does have an @Inject-annotated constructor you should still verify it is not provided via a module since bindings provided via a module can implicitly override bindings provided via an @Inject-annotated constructor.

    To understand this behavior, it is helpful to think of the @Inject-annotated constructor and class scope annotations as a single definition, just like the @Provides method and its scope annotations are a single definition. Rather than trying to merge the two definitions (using the scope from the class-annotation definition and the implementation from the @Provides method definition), Dagger just observes the scopeless @Provides method definition and entirely overrides the class-annotation definition.

    Dagger behaves as if you are deliberately trying to override the scope on ToastMaker to make it scopeless--which is a particularly reasonable assumption because you can customize your set of modules per Dagger component but you can only define your potentially-reusable ToastMaker class exactly once. If you need to override the class, including scopes defined directly on the class, then a @Provides method on a module is your natural choice and you absolutely wouldn't want the scope definition on the class to be merged into your module override.

    To fix this problem, you'll need to either remove the @Provides method (such that the @Inject constructor and scope remain together), or add the appropriate @MyScope annotation onto the @Provides method.