Dagger2,在依赖组件中为 ViewModelProvider.Factory 添加绑定

Dagger2, adding a binding for ViewModelProvider.Factory in a dependant component

问题

当尝试将 ViewModel 绑定添加到较低范围 (@FragmentScope) 内继承的 ViewModelFactory(在没有范围的情况下创建)的多重绑定时,我保持 运行 进入这个错误:

java.lang.IllegalArgumentException: unknown model class com.example.app.MyFragmentVM

我读过或尝试过的内容

(注意:以下绝不是详尽无遗的列表,而是我仔细阅读过的资源和建议类型的两个很好的例子)

我对使用 Dagger 还比较陌生,所以我不得不进行大量谷歌搜索以尝试了解发生了什么,但我已经达到了一个地步,据我所知,应该有一些东西可以工作(?)...

从类似于 [1] 的来源中,我删除了 ViewModelFactory 上的 @Singleton 范围,但我仍然遇到上述崩溃,表示在映射中找不到模型 class .

从类似于 [2] 的来源,我试图加强我对依赖项如何工作以及项目如何暴露给依赖组件的理解。我知道并理解我的 MyFragmentComponent 及其相关模块如何使用 ViewModelProvider.Factory

但是我不明白为什么 @Binds @IntoMapMyFragmentVM 不起作用。

代码

让我先过一遍应用程序中已经存在的东西的设置 -- 其中几乎 none 是针对特定情况的范围

// 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)
        }
    }
}

以下是我尝试添加和使用我的@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)
    }
}

这里有趣的是 MyFragmentModule 本身并没有为 MyFragment 提供任何独特的注入(这些都来自 AppComponent,就像现在一样)。但是,它确实为 MyFragment 使用的 ViewModel 提供了独特的注入。

这个问题的根源是子组件组件依赖关系之间的区别。

子组件

在使用子组件时,父组件了解其子组件的所有信息。因此,当子组件请求多重绑定时,父组件可以将其贡献与子组件的贡献结合起来。这甚至可以传递:如果子组件请求一个未限定范围的 ViewModelProvider.Factory,注入的映射将包含来自子组件的绑定。 (@Reusable 绑定也是如此,但 @Singleton 则不然。)

如果您将具有依赖关系的组件更改为子组件,一切都会正常进行。但是,这可能不适合您所需的架构。特别是,如果 MyFragmentComponent 在 Instant App 模块中,这是不可能的。

组件依赖关系

在处理组件依赖时,主组件只是通过提供方法公开对象,它不知道任何可能依赖它的组件。这一次,当请求 ViewModelProvider.Factory 时,主要组件无法访问除它自己之外的任何 @ViewModelKey 绑定,因此 Factory 它 returns 将不包括MyFragmentVM绑定。

如果 MyFragmentComponent 不需要来自 AppComponent 的任何 ViewModel 绑定,您可以将 bindsViewModelFactory 提取到它自己的模块中并将其包含在两个组件中。这样,两个组件都可以独立创建自己的 Factory

如果您确实需要来自 AppComponent 的一些 ViewModel 绑定,希望您也可以将这些绑定模块添加到 MyFragmentComponent。否则,您将不得不在 AppComponent 中公开地图,然后以某种方式将这些条目与您的新绑定结合起来。 Dagger 没有提供一个很好的方法来做到这一点。