Search code examples
androidkotlindependency-injectiondaggerandroid-viewmodel

Dagger/MissingBinding when Injecting Dependencies into a ViewModel


I'm trying to get my repository injected into my ViewModels. However, when compiling the code I keep getting this error. I'm not sure where to go with this...

C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\di\AppComponent.java:8: 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 {
                ^
  A binding with matching key exists in component: com.example.barrechat192.ui.fragments.barremap.di.BarreMapComponent
  A binding with matching key exists in component: com.example.barrechat192.ui.fragments.camera.di.CameraComponent
  A binding with matching key exists in component: com.example.barrechat192.ui.fragments.photoview.di.PhotoViewComponent
      java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is injected at
          com.example.barrechat192.di.FoundationViewModelFactory(creators)
      com.example.barrechat192.di.FoundationViewModelFactory is injected at
          com.example.barrechat192.di.ViewModelBuilderModule.bindViewModelFactory(factory)
      androidx.lifecycle.ViewModelProvider.Factory is injected at
          com.example.barrechat192.ui.fragments.photoeditor.PhotoEditorFragment.viewModelFactory
      com.example.barrechat192.ui.fragments.photoeditor.PhotoEditorFragment is injected at

As they do in the sample, I set up my App with a component and then create sub components for each fragment.


@Singleton
@Component(
    modules = [
        AppModule::class,
        ViewModelBuilderModule::class,
        SubcomponentsModule::class
    ]
)
interface AppComponent {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance applicationContext: Context) : AppComponent
    }

    fun barreMapComponent(): BarreMapComponent.Factory
    fun cameraComponent() : CameraComponent.Factory
    fun photoEditorComponent(): PhotoEditorComponent.Factory
    fun photoViewComponent(): PhotoViewComponent.Factory

}

@Module(
    subcomponents = [
        BarreMapComponent::class,
        CameraComponent::class,
        PhotoEditorComponent::class,
        PhotoViewComponent::class
    ]
)
object SubcomponentsModule

Each subcomponent has a Module that is related to the ViewModel it is injecting into. I'm showing one of the four.

@Subcomponent(modules = [BarreMapModule::class])
interface BarreMapComponent {

    @Subcomponent.Factory
    interface Factory{
        fun create() : BarreMapComponent
    }

    fun inject(fragment: BarreMapFragment)
}

@Module
abstract class BarreMapModule {

    @Binds
    @IntoMap
    @ViewModelKey(BarreMapViewModel::class)
    abstract fun bindViewModel(viewModel: BarreMapViewModel) : ViewModel

}

Finally there is the module dealing with ViewModelFactory injection,


class FoundationViewModelFactory @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)
        }
    }
}

@Module
abstract class ViewModelBuilderModule {

    @Binds
    abstract fun bindViewModelFactory(
        factory: FoundationViewModelFactory
    ): ViewModelProvider.Factory
}

@Target(
    AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

And these are injected into the fragments as such

class BarreMapFragment: Fragment() {


    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private val mapViewModel by viewModels<BarreMapViewModel> { viewModelFactory }

}


Solution

  • You may not need to have subcomponents for your fragments. Let me make the code very simpler for you...

    In your AppComponent class, replace your code with this:

    @Singleton
    @Component(modules = [ViewModelModule::class])
    interface ApplicationComponent {
    
        @Component.Builder
        interface Builder {
            @BindsInstance
            fun application(application: Application?): Builder?
            fun build(): ApplicationComponent?
        }
    
        fun inject(fragment: BarreMapFragment)
        // Other fragments or activities are included here
    }
    

    Create a ViewModelModule class to provide your ViewModelFactory and other ViewModel classes

    @Module
    abstract class ViewModelModule {
    
        @Binds
        abstract fun bindViewModelFactory(factory: 
        FoundationViewModelFactory): ViewModelProvider.Factory
    
    
        @Binds
        @IntoMap
        @ViewModelKey(BarreMapViewModel::class)
        abstract fun bindViewModel(viewModel: BarreMapViewModel) : ViewModel
    
        // Provide other ViewModel classes here
    
    }
    

    Then in your BarreMapViewModel class, inject your repository like:

    class ChartViewModel @Inject constructor(private val repository: Repository) : 
        ViewModel() {}
    

    Let me know if this helps.