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 to multibound
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 the subcomponent
, then it has the values or entries defined by the subcomponent’s multibindings
as well as those defined by the parent component’s multibindings
. 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.
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.
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
. But MyViewModel
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 a ViewModelFactory
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.
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 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:
/**
* Binds ViewModelFactory as ViewModelProvider.Factory.
*/
@Module
abstract class ViewModelProviderModule {
@Binds abstract fun bindsViewModelFactory(impl: ViewModelFactory): ViewModelProvider.Factory
}
/**
* For the concept, we bind a factory for an AppViewModel
* in a module that is included directly in the AppComponent.
*/
@Module
abstract class AppModule {
@Binds @IntoMap
@ViewModelKey(AppViewModel::class)
abstract fun bindsAppViewModel(vm: AppViewModel): ViewModel
}
/**
* This module will be included in the Activity Subcomponent.
*/
@Module
abstract class ActivityBindingsModule {
@Binds @IntoMap
@ViewModelKey(MyViewModel::class)
}
/**
* Generate an injector for injecting dependencies that are scoped to MyActivity.
* This will generate a @Subcomponent for MyActivity.
*/
@Module
abstract class MyActivityModule {
@ActivityScoped
@ContributesAndroidInjector(modules = [ActivityBindingsModule::class])
abstract fun myActivity(): MyActivity
}
If you were to inject ViewModelProvider.Factory
to your application class, then what should be provided in Map<Class<out ViewModel>, Provider<ViewModel>>
? Since you are injecting in the scope of AppComponent
, that ViewModelFactory
will only be able to create instances of AppViewModel
, and not MyViewModel
since the binding is defined in the subcomponent.
If you inject ViewModelProvider.Factory
in MyActivity
, then since we are both in the scope of AppComponent
and MyActivitySubcomponent
, then a newly created ViewModelFactory
will be able to create both instances of AppViewModel
and MyViewModel
.
The problem here is that ViewModelFactory
is annotated as @Singleton
. Because of this, a single instance of the ViewModelFactory is created and kept in the AppComponent
. Since MainActivityComponent
is a subcomponent of AppComponent
, it inherits that singleton and will not create a new instance that includes the Map with the 2 ViewModel
bindings.
Here is a sequence of what's happening:
MyApplication.onCreate()
is called. You create your DaggerAppComponent
.
- In
DaggerAppComponent
's constructor, Dagger builds a Map having a mapping for Class<AppViewModel>
to Provider<AppViewModel>
.
- It uses that Map as a dependency for
ViewModelFactory
, then saves it in the component.
- When injecting into the Activity, Dagger retrieves a reference to that
ViewModelFactory
and injects it directly (it does not modify the Map).
What you could do to make it work as expected
- Remove the
@Singleton
annotation on ViewModelFactory
. This ensures that Dagger will create a new instance of ViewModelFactory
each time it is needed. This way, ViewModelFactory
will receive a Map containing both bindings.
- Replace the
@Singleton
annotation on ViewModelFactory
with @Reusable
. This way, Dagger will attempt to reuse instances of ViewModelProvider
, 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 each AppComponent
and MyActivitySubcomponent
.