#android #kotlin #dagger-hilt
Вопрос:
Основываясь на руководстве по Hilt, ViewModels необходимо вводить следующим образом:
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
Однако в моем случае я хочу использовать интерфейс:
interface ExampleViewModel()
@HiltViewModel
class ExampleViewModelImp @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ExampleViewModel, ViewModel() {
...
}
Затем я хочу ввести его через интерфейс
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
private val exampleViewModel: ExampleViewModel by viewModels()
...
}
Как заставить это работать?
Ответ №1:
viewModels
требуется ребенок из ViewModel
класса
val viewModel: ExampleViewModel by viewModels<ExampleViewModelImp>()
Ответ №2:
У меня была аналогичная проблема, когда я хотел внедрить ViewModel через интерфейс, в первую очередь из-за того, чтобы переключить его с поддельной реализацией во время тестирования. Мы переходим с Dagger Android на Hilt, и у нас были тесты пользовательского интерфейса, в которых использовались поддельные модели представления. Добавляю свои выводы сюда, чтобы это могло помочь кому-то, кто столкнулся с подобной проблемой.
- Оба
by viewModels()
иViewModelProviders.of(...)
ожидает тип, который расширяетсяViewModel()
. Таким образом, интерфейс будет невозможен, но мы все равно можем использовать абстрактный класс, который расширяетViewModel()
- Я не думаю, что есть способ использовать
@HiltViewModel
для этой цели, так как не было возможности переключить реализацию. - Поэтому вместо этого попробуйте ввести
ViewModelFactory
его вFragment
. Вы можете переключить завод во время тестирования и тем самым переключить модель представления.
@AndroidEntryPoint
class ListFragment : Fragment() {
@ListFragmentQualifier
@Inject
lateinit var factory: AbstractSavedStateViewModelFactory
private val viewModel: ListViewModel by viewModels(
factoryProducer = { factory }
)
}
abstract class ListViewModel : ViewModel() {
abstract fun load()
abstract val title: LiveData<String>
}
class ListViewModelImpl(
private val savedStateHandle: SavedStateHandle
) : ListViewModel() {
override val title: MutableLiveData<String> = MutableLiveData()
override fun load() {
title.value = "Actual Implementation"
}
}
class ListViewModelFactory(
owner: SavedStateRegistryOwner,
args: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, args) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return ListViewModelImpl(handle) as T
}
}
@Module
@InstallIn(FragmentComponent::class)
object ListDI {
@ListFragmentQualifier
@Provides
fun provideFactory(fragment: Fragment): AbstractSavedStateViewModelFactory {
return ListViewModelFactory(fragment, fragment.arguments)
}
}
@Qualifier
annotation class ListFragmentQualifier
Вот ListViewModel
абстрактный класс и ListViewModelImpl
его фактическая реализация. Вы можете переключить ListDI
модуль во время тестирования с помощью TestInstallIn
. Для получения дополнительной информации об этом и рабочем проекте обратитесь к этой статье
Комментарии:
1. Я нашел более простое решение. Проверьте мой ответ 😉
Ответ №3:
Нашел решение, используя HiltViewModel
в качестве прокси-сервера фактический класс, который я хочу внедрить. Это просто и работает как шарм 😉
Модуль
@Module
@InstallIn(ViewModelComponent::class)
object MyClassModule{
@Provides
fun provideMyClas(): MyClass = MyClassImp()
}
class MyClassImp : MyClass {
// your magic goes here
}
Фрагмент
@HiltViewModel
class Proxy @Inject constructor(val ref: MyClass) : ViewModel()
@AndroidEntryPoint
class MyFragment : Fragment() {
private val myClass by lazy {
val viewModel by viewModels<Proxy>()
viewModel.ref
}
}
Теперь вы получили myClass
интерфейс типа MyClass
, ограниченный viewModels<Proxy>()
жизненным циклом
Комментарии:
1. Возможно, я неправильно понял вопрос, но
Proxy
он все еще не задан? Модель представленияProxy
не вводится в качестве интерфейса. Но, скорее, одна из зависимостей ViewModelMyClass / MyClassImpl
вводится в качестве интерфейса. Таким образом, любая логика, которая у нас есть в классе ViewModelProxy
, не может быть изменена во время тестирования, верно? Требование, которое у меня было, состояло в том, чтобы поменятьProxy
себя во время тестированияFakeProxy
вместоProxyImpl
.2.@Генри вы частично правы 🙂 Да, класс прокси нельзя поменять местами для тестирования, но именно поэтому это прокси. Вся бизнес — логика из
Proxy
ViewModel
этого была перенесена вMyClass
. Каждый, кто хочет использовать/тестироватьMyClass
, создаст свою собственнуюProxy
модель представления для проксиMyClass
-сервера . Внутри меня нет логикиProxy
, и это буквально одно строчное определение.3. Чтобы было понятно при создании теста, вам нужно использовать
TestInstallIn
для замены тестовыйMyClassModule
модуль, который сопоставляетсяMyClass
MyTestclassImp
.4. Хорошо, так что дополнительный уровень косвенности. Затем мы должны передать определенные функции ViewModel из прокси в MyClass, такие как SavedStateHandle, ViewModelScope и т. Д., Поскольку MyClass не является ViewModel.
5. @Генри да, это недостаток, но вы можете построить его красиво , чтобы ваша инфраструктура(
super of MyClass
) имела ссылку наViewModel
, это просто слишком много для этого ответа 😉
Ответ №4:
Это так просто ввести интерфейс, вы передаете интерфейс, но инъекция вводит Импл.
@InstallIn(ViewModelComponent::class)
@Module
class DIModule {
@Provides
fun providesRepository(): YourRepository = YourRepositoryImpl()
}
Комментарии:
1.
ViewModelComponent
это неViewModel
так .ViewModelComponent
необходимо вводить внутрьViewModel
2.
@InstallIn(ViewModelComponent::class)
Это область видимости модели