Search code examples
androidkotlindagger-2

Dagger 2 injecting view model of activity into fragment


(using kotlin) I have an app that uses a settings activity with 2 fragments. I'd like both to get the same instance of the SettingsViewModel as the activity. I assume there's a scoping issue that I'm missing.

First, I have the standard ViewModelModule:

@Module
abstract class ViewModelModule {
    @Binds @IntoMap
    @ViewModelKey(SettingsViewModel::class)
    abstract fun bindSettingsViewModel(viewModel: SettingsViewModel): ViewModel

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

I bind my activities in:

@Module
abstract class AndroidBindingModule {
    @PerActivity
    @ContributesAndroidInjector(modules = [SettingsActivityModule::class])
    abstract fun contributeSettingsActivity(): SettingsActivity
}

With all other things in place this works well and SettingsActivity does get an instance of the SettingsViewModel. SettingsActivityModule adds the following:

@PerFragment
@ContributesAndroidInjector
abstract fun contributeMainSettingsFragment(): MainSettingsFragment

@PerFragment
@ContributesAndroidInjector
abstract fun contributeDebugSettingsFragment(): DebugSettingsFragment

Both fragments appear to have the injectors called on them (I've checked through the debugger and AndroidSupportInjection.inject(fragment) is called). The fragments include:

@Inject lateinit var mainViewModel: SettingsViewModel

But in my fragment's onCreate() I'm seeing that mainViewModel is still null. Is there anything special I need to do here to avoid calling ViewModelProviders.of(activity)[SettingsViewModel::class.java] and instead injecting the view model?

UPDATE:

After a bit more reading I found that the correct way of using the view model injection in fragments is to inject the factory and get the view model in onActivityCreated:

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var mainViewModel: SettingsViewModel

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    mainViewModel = ViewModelProviders.of(activity, viewModelFactory)[SettingsViewModel::class.java]
}

This makes sense since I have MyViewModelFactory bound as the ViewModelProvider.Factory and it's annotated with @Singleton. When I try to compile the above I get the following error:

Error:(6, 1) error: [dagger.android.AndroidInjector.inject(T)] java.util.Map<kotlin.reflect.KClass<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.

It appears that Dagger can't find the mapping created by the ViewModelModule. I'm still at a loss at how that can be. Maybe my tree is incorrect? Why would activities in AndroidBindingModule be able to get the ViewModel but not fragments?

AppComponent
  - AndroidInjectionModule
  - AndroidBindingModule
  - AppModule
    - SdkModule
    - ViewModelModule
    - GotItCardModule
    - ViewHolderSubcomponent (provides a mapping of layout ID -> ViewHolder for a factory)

UPDATE

I've done a little more digging into this... From the full error:

e: /home/user/workspace/Example/sdktest/build/tmp/kapt3/stubs/debug/com/example/sdktest/di/AppComponent.java:6: error: [dagger.android.AndroidInjector.inject(T)] java.util.Map<kotlin.reflect.KClass<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.
e: 

e: public abstract interface AppComponent {
e:                 ^
e:       java.util.Map<kotlin.reflect.KClass<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> is injected at
e:           com.example.sdktest.di.viewmodel.ExampleViewModelFactory.<init>(creators)
e:       com.example.sdktest.di.viewmodel.ExampleViewModelFactory is injected at
e:           com.example.sdktest.di.viewmodel.ViewModelModule.bindViewModelFactory(factory)
e:       android.arch.lifecycle.ViewModelProvider.Factory is injected at
e:           com.example.sdktest.ui.settings.fragment.MainSettingsFragment.viewModelFactory
e:       com.example.sdktest.ui.settings.fragment.MainSettingsFragment is injected at
e:           dagger.android.AndroidInjector.inject(arg0)

I think the issue is that somehow Dagger is trying to inject my fragment with dagger.android.AndroidInjecton instead of dagger.android.AndroidSupportInjection. Still not sure how to fix.


Solution

  • OK, so I found the answer and it wasn't anywhere near what I thought. My translation of the GithubViewModelFactory into Kotlin included the following constructor:

    @Singleton
    class MetaverseViewModelFactory @Inject constructor(
            creators: Map<KClass<out ViewModel>, Provider<ViewModel>>
    ): ViewModelProvider.Factory {
        private val creators: Map<Class<out ViewModel>, Provider<ViewModel>> =
                creators.mapKeys { it.key.java }
        //...
    }
    

    This was due to the ViewModelKey in Kotlin being able to use KClass only instead of Class. It turns out that kapt takes care of this and the correct factory should look like:

    @Singleton
    class MetaverseViewModelFactory @Inject constructor(
            private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
    ): ViewModelProvider.Factory {
        //...
    }
    

    Note the additional @JvmSupressWildcards to also avoid turning Provider<ViewModel> into Provider<? extends ViewModel>