Рукоять — Как внедрить интерфейс ViewModel?

#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, и у нас были тесты пользовательского интерфейса, в которых использовались поддельные модели представления. Добавляю свои выводы сюда, чтобы это могло помочь кому-то, кто столкнулся с подобной проблемой.

  1. Оба by viewModels() и ViewModelProviders.of(...) ожидает тип, который расширяется ViewModel() . Таким образом, интерфейс будет невозможен, но мы все равно можем использовать абстрактный класс, который расширяет ViewModel()
  2. Я не думаю, что есть способ использовать @HiltViewModel для этой цели, так как не было возможности переключить реализацию.
  3. Поэтому вместо этого попробуйте ввести 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 не вводится в качестве интерфейса. Но, скорее, одна из зависимостей ViewModel MyClass / MyClassImpl вводится в качестве интерфейса. Таким образом, любая логика, которая у нас есть в классе ViewModel Proxy , не может быть изменена во время тестирования, верно? Требование, которое у меня было, состояло в том, чтобы поменять 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) Это область видимости модели