#android #android-viewmodel #dagger-hilt
Вопрос:
Я пытаюсь разобраться в Хилте и в том, как он работает с моделями просмотра. Я бы хотел, чтобы мои фрагменты зависели от абстрактных моделей представления, чтобы я мог легко издеваться над ними во время тестов пользовательского интерфейса. Экс:
@AndroidEntryPoint
class MainFragment : Fragment() {
private val vm : AbsViewModel by viewModels()
/*
...
*/
}
@HiltViewModel
class MainViewModel(private val dependency: DependencyInterface) : AbsViewModel()
abstract class AbsViewModel : ViewModel()
Есть ли способ настроить ViewModels() таким образом, чтобы он мог сопоставлять конкретные реализации с абстрактными моделями представлений? Или передать пользовательский завод-производитель в ViewModels (), который может сопоставлять конкретные экземпляры моделей представления с абстрактными классами?
Точный вопрос также доступен здесь, но он довольно старый, учитывая, что хилт тогда еще был в альфе: https://github.com/google/dagger/issues/1972 Однако предлагаемое там решение не очень желательно, поскольку в нем используется строка, указывающая на путь конкретной модели представления. Я думаю, что это не переживет запутывания или перемещения файлов, и это может быстро превратиться в кошмар для обслуживания. В ответе также предлагается ввести конкретную модель представления во фрагмент во время тестов со всеми насмешливыми зависимостями модели представления, тем самым получив возможность контролировать то, что происходит в тесте. Это автоматически делает мой тест пользовательского интерфейса зависимым от реализации указанной модели представления, чего я бы очень хотел избежать.
Невозможность использовать абстрактные модели представления в своих фрагментах заставляет меня думать, что я нарушаю принципы D в ТВЕРДЫХ принципах, чего я также хотел бы избежать.
Ответ №1:
Не самое чистое решение, но вот что мне удалось сделать.
Сначала создайте ViewModelClassesMapper, который поможет сопоставить абстрактный класс с конкретным. В моем случае я использую пользовательскую модель AbsViewModel, но ее можно заменить обычной моделью просмотра. Затем создайте поставщика пользовательских моделей представлений, который зависит от указанного выше сопоставителя.
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.
@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
}
Это долгий путь, но после завершения начальной настройки все, что вам нужно сделать для отдельных фрагментов, — это сделать их зависимыми от виртуальной машины и предоставить конкретный класс для вашей модели просмотра в Hilt с помощью клавиши @ViewModel.
В тестах vmProvider можно легко высмеять и заставить выполнять ваши требования.