Search code examples
androiddagger-2android-architecture-componentsandroid-viewmodel

ViewModel cannot be provided without an @Inject constructor or an @Provides-annotated


Question EDITED

I am injecting ViewModelProvider.Factory to BaseActivity like below

open class BaseActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var factories: ViewModelProvider.Factory

    inline fun <reified T : ViewModel> getViewModel(): T {
        return ViewModelProvider(this, factories).get(T::class.java)
    }
}

viewModel only works when we inject then like below.

class MainViewModel @Inject constructor( private val alertStore: AlertStore)
    : BaseViewModel(){

    fun showDialog(){
        viewModelScope.launch {
            delay(4000)

            alertStore.showToast("Alert after 4 seconds.")
        }
    }
}

Why this @Inject constructor is necessary in my current implementation

class MainActivity : BaseActivity() {

  private lateinit var viewModel: MainViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    viewModel = getViewModel()
    viewModel.showDialog()
}

}

App.kt

class App : DaggerApplication() {

override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
    return DaggerAppComponent.builder().addContext(this).build()
    }
}

AppComponent.kt

@Component(
    modules = [
        AndroidInjectionModule::class,
        AppModule::class,
        ActivityBuilder::class,
        ViewModelInjector::class

    ]
)
@Singleton
interface AppComponent : AndroidInjector<App> {

    @Component.Builder
    interface Builder {

        fun addContext(@BindsInstance context: Context): Builder

        fun build(): AppComponent
    }
}

AppModule.kt

@Module
class AppModule {

    @Provides
    fun provideViewModelFactories(viewModels: Map<Class<out ViewModel>,
            @JvmSuppressWildcards Provider<ViewModel>>):
            ViewModelProvider.Factory {
        return object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                val factory = viewModels[modelClass]?.get() ?: error(
                    "No factory provided against ${modelClass.name}"
                )
                @Suppress("UNCHECKED_CAST")
                return factory as T
            }
        }
    }
}

ActivityBuilder.kt

@Module
abstract class ActivityBuilder {

    //@Scope("")
    @ContributesAndroidInjector ///(modules = {MainModelFactory.class})
    public abstract MainActivity bindMainActivity();
}

ViewModelInjector.kt

@Module public abstract class ViewModelInjector {

@Binds
@IntoMap
@ViewModelKey(MainViewModel.class)
public abstract ViewModel providesMainViewModel(MainViewModel model);

}

ViewModelKey.kt

@MapKey
@Retention(AnnotationRetention.SOURCE)
annotation class ViewModelKey(
    val value: KClass<out ViewModel>
)

Why do I have to append @Inject constructor to each ViewModel and kindly explain a little why we need @Binds @IntoMap and with ViewModel


Solution

  • When you use dagger android you should make your activities and fragments as extensions of DaggerActivity (and DaggerFragment for fragments).

    class MainActivity : DaggerActivity() {
    
        @Inject
        lateinit var viewModel: MainViewModel
    }
    

    Next you should prepare infrastructure for injection:

    1. Create injectors for each your activity:

      // All your injectors can be defined in this module
      @Module(includes = [AndroidInjectionModule::class])
      interface AppInjectorModule {
      
          @ContributesAndroidInjector(modules = [MainActivityVmModule::class, /*other dependecies*/])
          fun getMainActivityInjector(): MainActivity
      }
      
    2. Create modules to provide view models (can be multiple for one activity) and factory

      @Module(includes = [VmFactoryModule::class])
      abstract class MainActivityVmModule {
      
          // bind implementation of ViewModel into map for ViewModelFactory
          @Binds
          @IntoMap
          @ClassKey(MainViewModelImpl::class)
          abstract fun bindMainVm(impl: MainViewModelImpl): ViewModel
      
          @Module
          companion object {
              @Provides
              @JvmStatic
              fun getMainVm(activity: MainActivity, factory: ViewModelProvider.Factory): MainViewModel {
                  // create MainViewModelImpl in scope of MainActivity and inject dependecies by ViewModelFactory
                  return ViewModelProviders.of(activity, factory)[MainViewModelImpl::class.java]
              }
          }
      }
      

      Factory can be provided by different module to avoid duplication

      @Module
      interface VmFactoryModule {
      
          @Binds
          // bind your implementation of factory
          fun bindVmFactory(impl: ViewModelFactory): ViewModelProvider.Factory
      }
      
    3. Add activities injectors to AppComponent graph
      @Component(
          modules = [
              AppInjectorModule::class
          ]
      )
      @Singleton
      interface AppComponent : AndroidInjector<App>
      

    Additional info: Dagger & Android