I'm using the well-known Dagger-ViewModelFactory pattern to be able to inject a factory for all the ViewModel
in all the activities.
@ActivityScope
class ViewModelFactory @Inject constructor(
private val creators: MutableMap<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
return creator.get() as T
}
}
The problem I have is that when I inject the factory into an Activity
Dagger fails because the providers of the objects for the ViewModels
that I'm not going to use are not always accessible. They are not because the modules that contain the providers have not been added.
For example, I have a LogIn activity and a SignUp activity, and this is the way I add the subcomponents for them:
@ContributesAndroidInjector(modules = [
ViewModelModule::class,
FirebaseModule::class,
LogInModule::class,
BindLogInModule::class
])
@ActivityScope
internal abstract fun loginActivityInjector(): LoginActivity
@ContributesAndroidInjector(modules = [
ViewModelModule::class,
FirebaseModule::class,
SignUpModule::class,
BindSignUpModule::class
])
@ActivityScope
internal abstract fun signUpActivityInjector(): SignUpActivity
Please notice that when I create the subcomponent for SignUpActivity
I do not add the Module LogInModule
because I do not need the bindings in that Module.
The result is that I get the error
e: com.package.my.AppComponent.java:8: error: [Dagger/MissingBinding] com.package.my.login.domain.LogInAuthenticator cannot be provided without an @Provides-annotated method. public abstract interface AppComponent extends dagger.android.AndroidInjector { ^ A binding with matching key exists in component: com.package.my.di.ActivityInjectorsModule_LoginActivityInjector$app_prodDebug.LoginActivitySubcomponent com.package.my.login.domain.LogInAuthenticator is injected at com.package.my.login.repository.LoginRepository(logInAuthenticator) com.package.my.login.repository.LoginRepository is injected at com.package.my.login.domain.LoginUseCase(loginRepository) com.package.my.login.domain.LoginUseCase is injected at com.package.my.login.presentation.LoginViewModel(loginUseCase) com.package.my.login.presentation.LoginViewModel is injected at com.package.my.di.ViewModelModule.provideLoginViewModel(viewModel) java.util.Map,javax.inject.Provider> is injected at com.package.my.di.ViewModelFactory(creators) com.package.my.di.ViewModelFactory is injected at com.package.my.di.ViewModelModule.bindViewModelFactory$app_prodDebug(factory) androidx.lifecycle.ViewModelProvider.Factory is injected at com.package.my.login.ui.SignUpActivity.viewModelFactory com.package.my.login.ui.SignUpActivity is injected at dagger.android.AndroidInjector.inject(T) [com.package.my.di.AppComponent → com.package.my.di.ActivityInjectorsModule_SignUpActivityInjector$app_prodDebug.SignUpActivitySubcomponent]
This happens because LogInAuthenticator
is provided by LogInModule
.
Does this mean that the only solution is to add LogInModule
even if I don't really need to create GoogleSignInClient
in the SignUpActivity
?
You have declared both of @ContributesAndroidInjector
methods to be dependent on ViewModelModule
. Inside ViewModelModule
you have declared all of the ViewModels
out there, which means, that at the point when Dagger wants to construct the dependency tree for SignUpActivity
it will also require you to explicitly mention how LoginViewModel
should be constructed. This happens, because Dagger needs to know how each of the dependency declared inside ViewModelModule
should be constructed.
The solution for you case will be either include all of the modules in all of @ContributesAndroidInjector
declarations (which is an ugly approach), or, alternatively, move the provider method of SignUpViewModel
to SignUpModule
and do not include ViewModelModule
for SignUpActivity
declaration.
Here's the setup that works for me.
First, I have created a BaseActivityModule
, which all of feature modules should include in their dedicated @Module
classes:
@Module
abstract class BaseActivityModule {
@Binds abstract fun bindsViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}
Then, assuming we have 2 features: Foo and Bar:
@Module
abstract class ActivitiesModule {
@PerActivity @ContributesAndroidInjector(modules = [FooModule::class])
abstract fun contributesFooActivity(): FooActivity
@PerActivity @ContributesAndroidInjector(modules = [BarModule::class])
abstract fun contributesBarActivity(): BarActivity
}
The implementation class of ViewModelProvider.Factory
should be scoped with @PerActivity
because the same instance of ViewModelProvider.Factory
should be provided each time that dependency is needed to be injected in the scope of particular activity:
private typealias ViewModelProvidersMap = Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
@PerActivity
class MyViewModelFactory @Inject constructor(
private val creators: ViewModelProvidersMap
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
var viewModelProvider = creators[modelClass]
if (viewModelProvider == null) {
val entries = creators.entries
val mapEntry = entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
} ?: throw IllegalArgumentException("Unknown model class $modelClass")
viewModelProvider = mapEntry.value
}
try {
@Suppress("UNCHECKED_CAST")
return viewModelProvider.get() as T
} catch (e: Throwable) {
throw IllegalArgumentException("Couldn't create ViewModel with specified class $modelClass", e)
}
}
}
Where @PerActivity
is declared this way:
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class PerActivity
FooModule
and BarModule
are declared as such:
@Module(includes = [BaseActivityModule::class])
abstract class FooModule {
@Binds @IntoMap @ViewModelKey(FooViewModel::class)
abstract fun bindsFooViewModel(viewModel: FooViewModel): ViewModel
}
@Module(includes = [BaseActivityModule::class])
abstract class BarModule {
@Binds @IntoMap @ViewModelKey(BarViewModel::class)
abstract fun bindsBarViewModel(viewModel: BarViewModel): ViewModel
}
Then we are including ActivitiesModule
in the AppComponent
as such:
@Singleton
@Component(modules = [
AndroidInjectionModule::class,
ActivitiesModule::class
])
interface AppComponent {
...
}
With this approach we've moved the ViewModelProvider.Factory
creation one layer down: previously it was in the topmost AppComponent
and now each of subcomponents will take care of creating the ViewModelProvider.Factory
.