Search code examples
androidkotlindagger-2

How to resolve bindings with different scopes?


I have a model (AModel) which I wanted to use as singleton. I created a custom scope named @ApplicationScope to use it for every class I need only once. So AppComponent and AModel shares this @ApplicationScope. I have some fragment (ConfirmationFragment) where I would like to use both AModel and BModel. BModel has a different scope because I'd like to use it in 3 fragments but Amodel needed everywhere.

The idea to access both AModel and BModel was to let ConfirmationComponent depend on AppComponent where AModel already available. In this way if I inject ConfirmationComponent to ConfirmationFragment I could use AModel, too.
But I got the following error: [Dagger/IncompatiblyScopedBindings] ConfirmationComponent scoped with @ConfirmationScope may not reference bindings with different scopes:

Build succeeds when injecting AModel is commented out from ConfirmationFragment but fails when it isn't. I need AModel in that fragment, too.

How could I fix this problem?
(In case it's important: I use only one activity, and let Android navigation do the work with fragments.)

open class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        val appComponent = initializeComponent()
    }

    val appComponent: AppComponent by lazy {
        initializeComponent()
    }

    val confirmationComponent: ConfirmationComponent by lazy {
        initializeConfirmationComponent()
    }

    open fun initializeComponent(): AppComponent {
        return DaggerAppComponent.factory().create(applicationContext)
    }

    open fun initializeConfirmationComponent(): ConfirmationComponent {
        return DaggerConfirmationComponent.builder().appComponent(appComponent).build()
    }
}

@ApplicationScope
@Component(modules = [NetworkModule::class])
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance context: Context) : AppComponent
    }
    fun inject(activity: MainActivity)
    fun inject(fragment: ConfirmationFragment)
}

@ConfirmationScope
@Component(dependencies = [AppComponent::class])
interface ConfirmationComponent {
    fun inject(fragment: ConfirmationFragment)
}

@ApplicationScope
class AModel @Inject constructor() {}

@ConfirmationScope
class BModel @Inject constructor() {}

class ConfirmationFragment : Fragment() {
    @Inject
    lateinit var modelA : AModel

    @Inject
    lateinit var modelB : BModel

    override fun onAttach(context: Context) {
        super.onAttach(context)
        (requireActivity().applicationContext as MyApplication).confirmationComponent.inject(this)
    }
    // Rest of the code
}

Solution

  • I think you should put it other way around. If I got you correctly ModelA has larger scope than ModelB this means you can have ModelB as a subcomponent of ModelA with narrower scope.

    So for this you would need:

    1. ConfirmationComponent
    //@YourScopeAnnotation
    @Subcomponent(modules = [...]) // if it is dependent on any modules
    interface ConfirmationComponent {
    
        // needed for dagger to create component
        @Subcomponent.Factory
        interface Factory {
            fun create(): ConfirmationComponent
        }
    
    
        fun inject(yourFragment: Fragment) // fun inject your fragment
    }
    
    1. SubcomponentsModule
    @Module(
        subcomponents = [ConfirmationComponent::class]
    )
    class SubcomponentsModule
    
    1. In your ApplicationComponent
    //@ApplicationScopeAnnotation  I think you can also use @Singleton
    @Component(
        modules = [NetworkModule::class, SubcomponentsModule::class]
    )
    interface ApplicationComponent {
    
        fun inject(activity: MainActivity)
        fun confirmationComponent(): ConfirmationComponent.Factory
    
    }
    
    1. Than just initialize ApplicationComponent in your Application class as usual

    2. Now in your main activity inject ModelA which should be available globally. Also create ConfirmationComponent in your activity

    @Inject
    lateinit var modelA : AModel
    
    lateinit var confirmationComponent: ConfirmationComponent
    
    override fun onCreate(savedInstanceState: Bundle?) {
        confirmationComponent = (applicationContext as MyApplication).appComponent
                .confirmationComponent()
                .create()
    
        modelA = (applicationContext as MyApplication).appComponent.inject(this)
    
    }
    
    1. Last step, in your fragment inject modelB and get modelA from activity
    @Inject
        lateinit var modelB: ModelB // inject modelB
    
        lateinit var modelA: ModelA // get ModelA from activity where it was already injected
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            (activity as MainActivity).confirmationComponent.inject(this)
    
            modelA = (activity as MainActivity).modelA
    
        }
    

    I hope this helps :)