问题
I've started using Dagger2, so there's still a lot to learn. I'm wondering if someone could point me on the right direction.
So, I've created a module for registering the view models used by my activities. It looks like this:
@Module
abstract class ViewModelModule {
@Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(MainActivityViewModel::class)
internal abstract fun bindMainActivityViewModel(viewModel: MainActivityViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ShowDetailsViewModel::class)
abstract fun bindShowDetaislViewModel(viewModel: ShowDetailsViewModel): ViewModel
}
ViewModelKey
is a simple helper annotation class which looks like this:
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey (val value: KClass<out ViewModel>) {
}
The ViewModelModule
is loaded by my main app component (used for creating the app):
@Singleton
@Component(
modules=[
AndroidSupportInjectionModule::class,
AppModule::class,
DatabaseModule::class,
NewsServiceModule::class,
JobBindingModule::class,
ViewModelModule::class,
PreferencesModule::class,
ActivityBindingModule::class
]
)
interface AppComponent: AndroidInjector<MyApp> {
@Component.Builder
abstract class Builder: AndroidInjector.Builder<MyApp>()
}
And here's the code for the ActivityBindingModule
, responsible for setting up the subcomponents (in this case, activities used by my app):
@Module
abstract class ActivityBindingModule {
@ActivityScoped
@ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity
@ActivityScoped
@ContributesAndroidInjector
internal abstract fun showDetailsActivity(): ShowDetailsActivity
}
Internally, each activity is instantiating the view model with code that looks like this (called from within the onCreate
method):
//view model code
_viewModel = ViewModelProviders.of(this, viewModelFactory)[ShowDetailsViewModel::class.java]
And, as you'd expect, viewModelFactory
is injected as field:
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
Both view models have external dependencies which are set up on the other modules referenced by the top app component.
And, for the sake of completeness, here's the code for my view model factory:
@Singleton
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T
= viewModels[modelClass]?.get() as T
This code works, but it seems like it can be improved. After reading the docs, I'm under the impression that I could refactor my ViewModeModule
so that it will simply instantiate my ViewModelFactory
and move each of the view model declarations into separate module (so that each of them can be injected only in the "correct" activity).
In order to test this, I've started by moving the ShowDetailsViewModel
into a new module which has only one entry:
@Module
internal abstract class DetailsModule {
@Binds
@IntoMap
@ViewModelKey(ShowDetailsViewModel::class)
abstract fun bindShowDetaislViewModel(viewModel: ShowDetailsViewModel): ViewModel
}
After that, the ViewModelModule looks like this:
@Module
abstract class ViewModelModule {
@Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(MainActivityViewModel::class)
internal abstract fun bindMainActivityViewModel(viewModel: MainActivityViewModel): ViewModel
}
And I've updated the ActivityBindingModule
so that in looks like this:
@Module
abstract class ActivityBindingModule {
@ActivityScoped
@ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity
@ActivityScoped
@ContributesAndroidInjector(modules = [DetailsModule::class])
internal abstract fun showDetailsActivity(): ShowDetailsActivity
}
Notice that now I'm passing the DetailsModule
(which instantiates the ShowDetailsViewModel
) to the ContributeAndroidInjector
annotation which is applied to the showDetailsActivity
method because that view model is only used by that activity.
Now, I'm surely missing something because after doing this, I'm always getting the following exception:
java.lang.IllegalStateException: ViewModelProviders.of(th…ilsViewModel::class.java] must not be null
If I debug the app, I can see that moving the ShowDetailsViewModel into its own model does not register it on the map used by the factory (ie, the map has only one entry, which corresponds to the MainActivityViewModel
that is registered in the ViewModelModule.
I thought that moving each view model the declaration into each a module used by a subcomponent should still allow it to be registered in a map injected by a module which is registered with the top component. Am I wrong? What is it that I'm missing to make this work?
Thanks.
回答1:
The problem lies with ViewModelFactory
being @Singleton
and that it won't get any of the bindings you add in your subcomponents. Remove the scope from your factory or make it @ActivityScoped
(the same scope as the ViewModel for the Activity)
The Activity (@ActivityScoped
) has access to the factory (@Singleton
), but the factory (@Singleton
) does not have access to use or create the ViewModel from a lower scope (@ActivityScoped
). So moving the factory to the same scope (@ActivityScoped
) would give it access to create the viewmodel in question.
来源:https://stackoverflow.com/questions/52348783/dagger2-and-android-load-module-which-injects-viewmodel-on-a-map