ViewModels 可以在 Android 上使用 Hilt 来初始化抽象 viewModel 字段吗?

Can Hilt be used on Android with by viewModels to initialize an abstract viewModel field?

我正在努力研究 Hilt 及其处理 ViewModel 的方式。 我希望我的片段依赖于抽象视图模型,这样我就可以在 UI 测试期间轻松地模拟它们。例如:

@AndroidEntryPoint
class MainFragment : Fragment() {
    private val vm : AbsViewModel by viewModels()

    /*
    ...
    */
}

@HiltViewModel
class MainViewModel(private val dependency: DependencyInterface) : AbsViewModel()

abstract class AbsViewModel : ViewModel()

有没有办法通过 viewModels() 进行配置,使其可以将具体实现映射到抽象视图模型?或者将自定义工厂生产者传递给 viewModels(),它可以将具体视图模型实例映射到抽象 类?

这里也有确切的问题,但考虑到 hilt 当时仍处于 alpha 阶段,它已经很老了:https://github.com/google/dagger/issues/1972 但是,那里提供的解决方案不是很理想,因为它使用指向具体视图模型路径的字符串。我认为这将无法在混淆或移动文件后幸存下来,并且很快就会成为维护的噩梦。答案还建议在测试期间将具体的视图模型注入到片段中,并模拟所有视图模型的依赖项,从而获得控制测试中发生的事情的能力。这会自动使我的 UI 测试依赖于所述视图模型的实现,我非常想避免这种情况。

无法在我的片段中使用抽象视图模型让我觉得我违反了 SOLID 原则中的 D,这也是我想避免的事情。

不是最干净的解决方案,但这是我设法做到的。

首先创建一个 ViewModelClassesMapper 来帮助将抽象 class 映射到具体的。在我的案例中,我使用的是自定义 AbsViewModel,但这可以换成常规 ViewModel。然后创建一个依赖于上述映射器的自定义视图模型提供程序。

class VMClassMapper @Inject constructor (private val vmClassesMap: MutableMap<Class<out AbsViewModel>, Provider<KClass<out AbsViewModel>>>) : VMClassMapperInterface {
    @Suppress("TYPE_INFERENCE_ONLY_INPUT_TYPES_WARNING")
    override fun getConcreteVMClass(vmClass: Class<out AbsViewModel>): KClass<out AbsViewModel> {
        return vmClassesMap[vmClass]?.get() ?: throw Exception("Concrete implementation for ${vmClass.canonicalName} not found! Provide one by using the @ViewModelKey")
    }
}

interface VMClassMapperInterface {
    fun getConcreteVMClass(vmClass: Class<out AbsViewModel>) : KClass<out AbsViewModel>
}

interface VMDependant<VM : AbsViewModel> : ViewModelStoreOwner {
    fun getVMClass() : KClass<VM>
}

class VMProvider @Inject constructor(private val vmMapper: VMClassMapperInterface) : VMProviderInterface {
    @Suppress("UNCHECKED_CAST")
    override fun <VM : AbsViewModel> provideVM(dependant: VMDependant<VM>): VM {
        val concreteClass = vmMapper.getConcreteVMClass(dependant.getVMClass().java)
        return ViewModelProvider(dependant).get(concreteClass.java) as VM
    }
}

interface VMProviderInterface {
    fun <VM :AbsViewModel> provideVM(dependant: VMDependant<VM>) : VM
}

@Module
@InstallIn(SingletonComponent::class)
abstract class ViewModelProviderModule {

    @Binds
    abstract fun bindViewModelClassesMapper(mapper: VMClassMapper) : VMClassMapperInterface

    @Binds
    @Singleton
    abstract fun bindVMProvider(provider: VMProvider) : VMProviderInterface

}

然后,使用自定义 ViewModelKey 注释映射您的具体 classes。

@Target(
        AnnotationTarget.FUNCTION,
        AnnotationTarget.PROPERTY_GETTER,
        AnnotationTarget.PROPERTY_SETTER
)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out AbsViewModel>)

@Module
@InstallIn(SingletonComponent::class)
abstract class ViewModelsDI {

    companion object {

        @Provides
        @IntoMap
        @ViewModelKey(MainContracts.VM::class) 
        fun provideConcreteClassForMainVM() : KClass<out AbsViewModel> = MainViewModel::class

        @Provides
        @IntoMap
        @ViewModelKey(SecondContracts.VM::class)
        fun provideConcreteClassForSecondVM() : KClass<out AbsViewModel> = SecondViewModel::class
    }

}

interface MainContracts {

    abstract class VM : AbsViewModel() {
        abstract val textLiveData : LiveData<String>
        abstract fun onUpdateTextClicked()
        abstract fun onPerformActionClicked()
    }

}

interface SecondContracts {

    abstract class VM : AbsViewModel()

}

最后,您使用抽象视图模型的片段如下所示:

@AndroidEntryPoint
class MainFragment : Fragment(), VMDependant<MainContracts.VM> {

    @Inject lateinit var vmProvider: VMProviderInterface

    protected lateinit var vm : MainContracts.VM

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        vm = vmProvider.provideVM(this)
    }

    override fun getVMClass(): KClass<MainContracts.VM> = MainContracts.VM::class

}

还有很长的路要走,但是在完成初始设置后,您需要为各个片段做的就是让它们实现 VMDependant 并为 Hilt 中的 YourAbsViewModel 使用提供具体的 class @ViewModelKey.

在测试中,vmProvider 可以很容易地被模拟并强制执行您的命令。