Search code examples
androidkotlindagger-2android-viewmodeldagger

How to make MVRX viewmodels work with dagger2?


Edit

Adding the @ViewModelKey and making sure all the viewmodels have the @Inject annotation did the trick

Injecting ViewModels using Dagger2 Di library and ViewModelFactory causing missing binding build error.

The error I am getting is the following:

 AppComponent.java:12: error: [Dagger/MissingBinding] java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method. public abstract interface AppComponent extends dagger.android.AndroidInjector<com.honing.daggerexploration.DaggerExplorationApplication> {
                ^
      java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is injected at
          com.honing.daggerexploration.di.DaggerViewModelFactory(creators)
      com.honing.daggerexploration.di.DaggerViewModelFactory is injected at
          com.honing.daggerexploration.features.MainActivity.viewModelFactory
      com.honing.daggerexploration.features.MainActivity is injected at
          dagger.android.AndroidInjector.inject(T) [com.honing.daggerexploration.di.AppComponent → com.honing.daggerexploration.di.modules.ActivityModule_BindActivityMain.MainActivitySubcomponent]

I have searched other stackoverflow questions but non of them solved the issue for me.

Dagger version I am using is the latest, 2.22.1

I bet this error is not related to MVRX as I was able to reproduce it in small library without involving mvrx view model class, however my intentions is to eventually use dagger2 with mvrx framework and be able to inject dependencies to it.

Some code related to this:

DaggerExplorationApplication

class DaggerExplorationApplication : DaggerApplication() {
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerAppComponent.builder().create(this)
    }

}

DaggerViewModelFactory:

    /**
     * ViewModelFactory which uses Dagger to create the instances.
     */
    class DaggerViewModelFactory @Inject constructor(
        private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
    ) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            var creator: Provider<out ViewModel>? = creators[modelClass]
            if (creator == null) {
                for ((key, value) in creators) {
                    if (modelClass.isAssignableFrom(key)) {
                        creator = value
                        break
                    }
                }
            }
            if (creator == null) {
                throw IllegalArgumentException("Unknown model class: $modelClass")
            }
            try {
                @Suppress("UNCHECKED_CAST")
                return creator.get() as T
            } catch (e: Exception) {
                throw RuntimeException(e)
            }
        }
    }

ViewModelFactoryModule

@Module
abstract class ViewModelFactoryModule {

    @Binds
    abstract fun bindViewModelFactory(viewModelFactory: DaggerViewModelFactory): ViewModelProvider.Factory
}

ActivityModule

@Module
abstract class ActivityModule {


    @ContributesAndroidInjector(modules = [ViewModelFactoryModule::class])
    abstract fun bindActivityMain(): MainActivity
}

In regards my efforts to implement mvrx with dagger, according to this I need to use AssistedInject library by square, I watched the video and fairly understand the reason behind this. Yet I am failed in making the project build because of the error described above. An interesting thread by chrisbanes also about this thing is on this link

MVRX ViewModels with dagger2 has been implemented successfully using this project(Tivi) by chrisbanes, I tried following what they did, but I failed too. The issue described at the top of the post is blocking me. Ready to provide any missing code, more info if needed to be able to resolve this issue.


Solution

  • You are missing the configuration for the map multi-binding.

    Tivi has the @ViewModelKey:

    /*
     * Copyright 2017 Google LLC
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package app.tivi.inject
    
    import androidx.lifecycle.ViewModel
    import dagger.MapKey
    import kotlin.reflect.KClass
    
    @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
    @Retention(AnnotationRetention.RUNTIME)
    @MapKey
    annotation class ViewModelKey(val value: KClass<out ViewModel>)
    

    And it has a module that binds the ViewModelKey to a particular subtype of ViewModel in such a way that it is exposed as ViewModel (and marked with a key):

    /*
     * Copyright 2017 Google LLC
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    @Binds
    @IntoMap
    @ViewModelKey(PopularShowsViewModel::class)
    abstract fun bindPopularShowsViewModel(viewModel: PopularShowsViewModel): ViewModel
    
    @Binds
    @IntoMap
    @ViewModelKey(TrendingShowsViewModel::class)
    abstract fun bindTrendingShowsViewModel(viewModel: TrendingShowsViewModel): ViewModel
    
    @Binds
    @IntoMap
    @ViewModelKey(ShowDetailsNavigatorViewModel::class)
    abstract fun bindDetailsNavigatorViewModel(viewModel: ShowDetailsNavigatorViewModel): ViewModel
    

    So you need to set these multi-binding configuration to a component using a module.

    And it is also important that their ViewModel classes have @Inject annotated constructor for this to work.