简体   繁体   中英

Dagger2 Inherited subcomponent multibindings

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 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:

  1. MyApplication.onCreate() is called. You create your DaggerAppComponent .
  2. In DaggerAppComponent 's constructor, Dagger builds a Map having a mapping for Class<AppViewModel> to Provider<AppViewModel> .
  3. It uses that Map as a dependency for ViewModelFactory , then saves it in the component.
  4. 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

  1. 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.
  2. 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 .

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 technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM