When attempting to add a ViewModel
bind into the multibinding for an inherited ViewModelFactory
(created with no scope) within a lower scope (@FragmentScope
), I keep running into this error:
java.lang.IllegalArgumentException: unknown model class com.example.app.MyFragmentVM
(note: the below is not by any means an exhaustive list, but are two good examples of resources and the kinds of advice I've perused)
I'm relatively new to working with Dagger so I had to do a lot of Googling to try and understand what has been going on, but I've reached a point where, to my understanding, something should be working(?)...
From sources similar to [1], I removed the @Singleton
scope on ViewModelFactory
, but I still get the aforementioned crash saying there is no model class found in the mapping.
From sources similar to [2] I tried to reinforce my understanding of how dependencies worked and how items are exposed to dependant components. I know and understand how ViewModelProvider.Factory
is available to my MyFragmentComponent
and it's related Modules.
However I do not understand why the @Binds @IntoMap
isn't working for the MyFragmentVM
.
Let me first go through the setup of the stuff that already exists in the application -- almost none of it was scoped for specific cases
// AppComponent
@Component(modules=[AppModule::class, ViewModelModule::class])
interface AppComponent {
fun viewModelFactory(): ViewModelProvider.Factory
fun inject(activity: MainActivity)
// ... and other injections
}
// AppModule
@Module
class AppModule {
@Provides
@Singleton
fun providesSomething(): Something
// a bunch of other providers for the various injection sites, all @Singleton scoped
}
// ViewModelModule
@Module
abstract class ViewModelModule {
@Binds
abstract fun bindsViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(MainActivityVM::class)
abstract fun bindsMainActivityVM(vm: MainActivityVM): ViewModel
}
// VMFactory
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)
}
}
}
And the following is how I am trying to add and utilize my @FragmentScope:
// MyFragmentComponent
@FragmentScope
@Component(
dependencies = [AppComponent::class],
modules = [MyFragmentModule::class, MyFragmentVMModule::class]
)
interface MyFragmentComponent {
fun inject(fragment: MyFragment)
}
// MyFragmentModule
@Module
class MyFragmentModule {
@Provides
@FragmentScope
fun providesVMDependency(): VMDependency {
// ...
}
}
// MyFragmentVMModule
@Module
abstract class MyFragmentVMModule {
@Binds
@IntoMap
@ViewModelKey(MyFragmentVM::class)
abstract fun bindsMyFragmentVM(vm: MyFragmentVM): ViewModel
}
// MyFragment
class MyFragment : Fragment() {
@set:Inject
internal lateinit var vmFactory: ViewModelProvider.Factory
private lateinit var viewModel: MyFragmentVM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerMyFragmentComponent.builder()
.appComponent(MyApplication.instance.component)
.build()
.inject(this)
viewModel = ViewModelProvider(this, vmFactory).get(MyFragmentVM::class.java)
}
}
What's interesting here to note is that MyFragmentModule
itself does NOT end up providing any unique injections for MyFragment
(those all come from AppComponent as it is right now). It DOES however, provide unique injections for the ViewModel
that MyFragment
uses.
The root of this problem is the difference between subcomponents and component dependencies.
When working with subcomponents, the parent component knows everything about its subcomponents. As such, when a subcomponent requests a multibinding, the parent component can combine its contributions with those of the subcomponent. This even works transitively: if the subcomponent requests an unscoped ViewModelProvider.Factory
, the injected map will include bindings from the subcomponent. (The same is true of a @Reusable
binding, but not a @Singleton
.)
If you change your components with dependencies into subcomponents, everything will just work. However, this might not fit your desired architecture. In particular, this is impossible if MyFragmentComponent
is in an Instant App module.
When working with component dependencies, the main component merely exposes objects through provision methods, and it does not know about any components that might depend on it. This time, when asked for a ViewModelProvider.Factory
, the main component does not have access to any @ViewModelKey
bindings except its own, and so the Factory
it returns will not include the MyFragmentVM
binding.
If MyFragmentComponent
does not require any ViewModel
bindings from AppComponent
, you can extract bindsViewModelFactory
into its own module and include it in both components. That way, both components can create their own Factory
independently.
If you do need some ViewModel
bindings from AppComponent
, hopefully you can add those binding modules to MyFragmentComponent
as well. If not, you would have to expose the map in AppComponent
, and then somehow combine those entries with your new bindings. Dagger does not provide a good way to do this.