Search code examples
androiddependency-injectionviewmodeldagger-2dagger

Injecting ViewModelFactory in different activities


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?


Solution

  • 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.