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