2
votes

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?

2
Please always include the full "stacktrace" of the error message and the code around it. I believe the issue arises due to adding the LoginViewModel to the SignUp Component. Usually you register the ViewModelFactory once (e.g. your AppComponent) then you add the viewmodel bindings to the components where you need them, so just the login vm to the login component, the sign up vm to the signup component, etc - David Medenjak
@DavidMedenjak I'm not adding the LoginViewModel to the SignUp subcomponent. If I do it then it works but I would prefer not to do it - kingston
I did not add the entire error because I think that the problem is clear. It is the solution that is not. Please also notice that I create the factory once too. The problem is that I need to add all the dependencies in all the subcomponents instead of just the necessary dependencies for each specific subcomponent. - kingston
I added now the entire error - kingston

2 Answers

3
votes

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.

0
votes

The answer to why you need to add the LoginModule lies in your error log. The error log traces the dependencies as below:

SignUpActivity <--is injected at-- ViewModelFactory <--is injected at-- LoginViewModel <--is injected at-- LoginUseCase <--is injected at-- LoginRepository <--is injected at-- LoginAuthenticator

The map above shows that you need to add LoginModule because Dagger needs it to successfully inject your ViewModelFactory into your SignUpActivity.

EDIT

Move SignUpViewModel binding from your ViewModelModule and place it in your SignUpModule like below:

@Module(includes = [SignUpModule.BindsModule::class])
class SignUpModule {

    // your other provides methods

    @Module
    interface BindsModule{

        @Binds
        @IntoMap
        @ViewModelKey(SignUpViewModel::class)
        fun signUpViewModel(signUpViewModel: SignUpViewModel): ViewModel
    }
}

Then add subcomponents this way. Notice ViewModelModule has been excluded

@ContributesAndroidInjector(modules = [
        FirebaseModule::class,
        SignUpModule::class,
        BindSignUpModule::class
    ])
    @ActivityScope
    internal abstract fun signUpActivityInjector(): SignUpActivity