Hope to find some help here after days and days researching about this very interested subject "Inherited subcomponent multibindings
which you can find here Inherited subcomponent multibindings which is the last subject in that page.
According to the official documentation:
subComponent
can add elements tomultibound
sets or maps that are bound in its parent. When that happens, the set or map is different depending on where it is injected. When it is injected into a binding defined on thesubcomponent
, then it has the values or entries defined by the subcomponent’smultibindings
as well as those defined by the parent component’smultibindings
. When it is injected into a binding defined on the parent component, it has only the values or entries defined there.
In other words. If the parent Component
has a multibound set or map
and a child component
has binding to that multibound, then those binding will be link/added into the parent map depending where those binding are injected within the dagger scope if any.
Here is the issue.
Using dagger version 2.24
in an Android Application using Kotlin
. I have an ApplicationComponent
making use of the new @Component.Factory
approach. The ApplicationComponent has installed the AndroidSupportInjectionModule
.
I also have an ActivitySubComponent
using the new @Component.Factory
approach and this one is linked to the AppComponent using the subComponents
argument of a Module annotation.
This ActivitySubComponent provides a ViewModel
thru a binding like this
@Binds
@IntoMap
@ViewModelKey(MyViewModel::class)
fun provideMyViewModel(impl: MyViewModel): ViewModel
the @ViewModelKey
is a custom Dagger Annotation.
I also have a ViewModelFactory implemented like this.
@Singleton
class ViewModelFactory @Inject constructor(
private val viewModelsToInject: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
viewModelsToInject[modelClass]?.get() as T
}
A normal ViewModelFactory
The difference here is that I am providing this ViewModelFactory in one of the AppComponents modules. But the bind viewModels within the ActivitySubComponent are not getting added into the ViewModelFactory Map in the AppComponent.
In other words. What the documentation is describing is not happening at all.
If I move the viewModels binding into any of the AppComponent Modules, then all work.
Do you know what could be happening here.
The documentation is accurate. While Dagger really operates the way it is described when generating Set/Map Multibindinds, it works differently for because you are in a corner case.
Explanation by example
Imagine you have the following modules:
If you were to inject
ViewModelProvider.Factory
to your application class, then what should be provided inMap<Class<out ViewModel>, Provider<ViewModel>>
? Since you are injecting in the scope ofAppComponent
, thatViewModelFactory
will only be able to create instances ofAppViewModel
, and notMyViewModel
since the binding is defined in the subcomponent.If you inject
ViewModelProvider.Factory
inMyActivity
, then since we are both in the scope ofAppComponent
andMyActivitySubcomponent
, then a newly createdViewModelFactory
will be able to create both instances ofAppViewModel
andMyViewModel
.The problem here is that
ViewModelFactory
is annotated as@Singleton
. Because of this, a single instance of the ViewModelFactory is created and kept in theAppComponent
. SinceMainActivityComponent
is a subcomponent ofAppComponent
, it inherits that singleton and will not create a new instance that includes the Map with the 2ViewModel
bindings.Here is a sequence of what's happening:
MyApplication.onCreate()
is called. You create yourDaggerAppComponent
.DaggerAppComponent
's constructor, Dagger builds a Map having a mapping forClass<AppViewModel>
toProvider<AppViewModel>
.ViewModelFactory
, then saves it in the component.ViewModelFactory
and injects it directly (it does not modify the Map).What you could do to make it work as expected
@Singleton
annotation onViewModelFactory
. This ensures that Dagger will create a new instance ofViewModelFactory
each time it is needed. This way,ViewModelFactory
will receive a Map containing both bindings.@Singleton
annotation onViewModelFactory
with@Reusable
. This way, Dagger will attempt to reuse instances ofViewModelProvider
, without guarantee that an unique instance is used accross the whole application. If you inspect the generated code, you will notice that a different instance is kept in eachAppComponent
andMyActivitySubcomponent
.You're scoping your
ViewModelProvider.Factory
as@Singleton
. This ensures that it will be created and kept within the@Singleton
component.It's safe to remove the scope since it doesn't keep any state, and it would allow the factory to be created where needed with the correct set of bindings.
When Dagger instantiates your ViewModelFactory, it needs to inject a map into its constructor. And for all the key/ViewModel pairs in the map, Dagger must know how to construct them at the CURRENT COMPONENT level.
In your case, your ViewModelFactory is only defined at the AppComponent level, so the map Dagger uses to inject it does not contain any ViewModel defined in its subcomponents.
In order for Dagger to exhibit the inherited subcomponent binding behaviour you expect, you must let your subcomponent provide the ViewModelFactory again, and inject your fragment/activity with the subcomponent.
When Dagger constructs the ViewModelFactory for your subcomponent, it has access to your ViewModels defined in the subcomponent, and therefore can add them to the map used to inject the factory.
You may like to reference Dagger's tutorial at page 10: https://dagger.dev/tutorial/10-deposit-after-login
Please notice how the tutorial uses the CommandRouter provided by the subcomponent to have the inherited multibinding.
The problem
It's because the map is being created in the AppComponent and you're adding the ViewModel to the map in a subcomponent. In otherwords, when the app starts it creates the map using the
ViewModelFactory
. ButMyViewModel
is not added to the map since it exists in a subcomponent.I struggled with this for quite a few days and I agree when you say the dagger documentation doesn't outline this very well. Intuitively you think that dependencies declared within the AppComponent are available to all subcomponents. But this is not true with Map Multibindings. Or at least not completely true.
MyViewModel
is not added to the map because the Factory that creates it exists inside the AppComponent.The solution (at least one possible solution)
Anyway, the solution I ended up implementing was I created feature-specific
ViewModelFactory
's. So for every subcomponent I created aViewModelFactory
that has it's own Key and set of multibindings.Example
I made a sample repo you can take a look at: https://github.com/mitchtabian/DaggerMultiFeature/
Checkout the branch: "feature-specific-vm-factories". I'll make sure I leave that branch the way it is, but I might change the master at some time in the future.