Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hilt - How to inject ViewModel interface?

Based on the Hilt tutorial, ViewModels needs to be inject the following way:

@HiltViewModel
class ExampleViewModel @Inject constructor(
  private val savedStateHandle: SavedStateHandle,
  private val repository: ExampleRepository
) : ViewModel() {
  ...
}

However, in my case, I want to use an interface:

interface ExampleViewModel()

@HiltViewModel
class ExampleViewModelImp @Inject constructor(
  private val savedStateHandle: SavedStateHandle,
  private val repository: ExampleRepository
) : ExampleViewModel, ViewModel() {
  ...
}

Then I want to inject it via the interface

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  private val exampleViewModel: ExampleViewModel by viewModels()
  ...
}

How to make this work?

like image 380
Ilya Gazman Avatar asked Mar 12 '26 20:03

Ilya Gazman


2 Answers

viewModels requires child of ViewModel class

val viewModel: ExampleViewModel by viewModels<ExampleViewModelImp>()
like image 147
IR42 Avatar answered Mar 15 '26 10:03

IR42


Had a similar problem where I wanted to Inject the ViewModel via interface, primarily because to switch it with a fake implementation while testing. We are migrating from Dagger Android to Hilt, and we had UI tests that used fake view models. Adding my findings here so that it could help someone whose facing a similar problem.

  1. Both by viewModels() and ViewModelProviders.of(...) expects a type that extends ViewModel(). So interface won't be possible, but we can still use an abstract class that extends ViewModel()
  2. I don't think there is a way to use @HiltViewModel for this purpose, since there was no way to switch the implementation.
  3. So instead, try to inject the ViewModelFactory in the Fragment. You can switch the factory during testing and thereby switch the ViewModel.
@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

Here, ListViewModel is the abstract class and ListViewModelImpl is the actual implementation. You can switch the ListDI module while testing using TestInstallIn. For more information on this, and a working project refer to this article

like image 20
Henry Avatar answered Mar 15 '26 10:03

Henry