Well, as I tried to summarise in the title, here is the details.
We have a relatively large application, that uses Dagger, in really not ideal ways, so we decided to start writing tests, and for that, I needed to expose dependencies for Mockito, hence, I faced an issue to start provide view models using a singleton factory, still applicable and there is tons of tutorials around that explains this.
We have across our app, a lot of features, that is implemented using a single activity, and a navigation component, that single activity sometimes have a created view model that we use to share data between the container activity and the fragments populated using the navigation editor.
What I couldn't figure out is the following, how can I use dagger to inject a shared view model, to return the same instance each time I invoke @Inject
for a specific view model, I know it could be done through scopes maybe, but I couldn't figure it out, and I have an explanation that I need to be verified. (I will provide my code below)
I started by implementing my Singleton ViewModelFactory as follows:
@Singleton
class ViewModelFactory @Inject constructor(private val creators: Map<Class<out ViewModel>,
@JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Then I created my ViewModelModule that provides the ViewModelFactory and the ViewModel as follows:
@Module
abstract class ViewModelFactoryModule {
@Binds
abstract fun bindsViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@EbMainScope
@ViewModelKey(EBMainContainerViewModel::class)
abstract fun bindEbMainViewModel(ebMainContainerViewModel: EBMainContainerViewModel): ViewModel
}
And before you ask, here is the scope implementation:
@Scope
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
annotation class EbMainScope
Last step, here is my activity/fragment injectors module:
@Module
abstract class ScreensBuildersModule {
@ContributesAndroidInjector
@EbMainScope
abstract fun contributeEbMainActivity(): EBMainActivity
@ContributesAndroidInjector
@EbMainScope
abstract fun contributeEbDashboardMainFragment(): EBDashboardMainFragment
}
Of course I wired everything in the AppComponent, and the app ran smoothly, with the catch, that there was two instances of EbMainContainerViewModel
, despite me defining the scope.
My explanation was, I actually had two different providers rather than one, but I still cannot understand why, since I marked it as @Singleton
.
Does someone has an explanation to this ? If more input is needed let me know guys.
@Reusable
annotation in place of@Singleton
. The details of why this fixed things is lost on me and I don't have time to figure this out ATM. I used@Reusable
on both the ViewModelFactory and the ViewModel class itself and that seemed to sort out duplicates for me. – orbitbotactivity?.run
to initiate a shared view model on the main thread, from the activity / fragment, the fragment would end up creating the same instance of viewmodel because it is already created on theactivity
, hence that activity object is the already holder Activity for the fragment, problem solved. – Omar K. Rostom