9

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?

Ilya Gazman
  • 31,250
  • 24
  • 137
  • 216

5 Answers5

6

viewModels requires child of ViewModel class

val viewModel: ExampleViewModel by viewModels<ExampleViewModelImp>()
IR42
  • 8,587
  • 2
  • 23
  • 34
4

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

Henry
  • 17,490
  • 7
  • 63
  • 98
1

Found a solution using HiltViewModel as a proxy to the actual class I wish to inject. It is simple and works like a charm ;)

Module

@Module
@InstallIn(ViewModelComponent::class)
object MyClassModule{
    @Provides
    fun provideMyClas(): MyClass = MyClassImp()
}

class MyClassImp : MyClass {
    // your magic goes here
}

Fragment

@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
    }
}

Now you got myClass of the type MyClass interface bounded to viewModels<Proxy>() lifeCycle

Ilya Gazman
  • 31,250
  • 24
  • 137
  • 216
  • Perhaps I misunderstood the question, but isn't `Proxy` still hard wired? The ViewModel `Proxy` is not injected as an Interface. But rather, one of the dependencies of the ViewModel, `MyClass / MyClassImpl` is injected as an Interface. So any logic that we have in the ViewModel class `Proxy` can't be modified during testing, right? The requirement I had was to swap `Proxy` itself during testing with `FakeProxy` instead of `ProxyImpl`. – Henry Oct 12 '21 at 04:07
  • @Henry you are partially right :) Yes, the Proxy class cannot be swapped for testing, but this is why it's a proxy. All the business logic from the `Proxy` `ViewModel` was moved to `MyClass`. Everyone who wishes to use/test `MyClass` will create their own `Proxy` viewModel to proxy `MyClass`. There I no logic inside `Proxy` and it is literally one line definition. – Ilya Gazman Oct 12 '21 at 17:40
  • Tobe clear when creating a test, you need to use `TestInstallIn` to replace `MyClassModule` with a test module that maps `MyClass` to `MyTestclassImp`. – Ilya Gazman Oct 12 '21 at 17:42
  • OK, so an additional level of indirection. We then have to pass ViewModel specific things from Proxy to MyClass, like SavedStateHandle, ViewModelScope, etc... since MyClass is not a ViewModel. – Henry Oct 23 '21 at 06:15
  • @Henry yep, this is the downside, but you can build it nicely such that your infrastructure(`super of MyClass`) will have a reference to the `ViewModel`, it's just too much for this answer ;) – Ilya Gazman Oct 24 '21 at 03:30
0
interface IProjectViewModel

abstract class AbstractViewModel(
    protected val savedStateHandle: SavedStateHandle
): ViewModel(), IProjectViewModel {

    private val _uiState = MutableStateFlow<Result<*>?>(null)
    val uiState: StateFlow<Result<*>>
        get() = _uiState

    // Why shouldn't we put this function signature in the IProjectViewModel interface ?
    protected fun execute(
        safeBlock: suspend () -> Result<*>
    ) { 
        viewModelScope.launch {
            flow {
                emit(Loading)
                emit(safeBlock.invoke())
            }.collect {
                _uiState.value = it
            }
        }
    }
}

/* 
* Notice IVM and VM are nullable, if you don't want to use ViewModel for your fragment, 
* then use Nothing? as the generic in your implementation fragment.
*/
abstract class AbstractFragment<VB : ViewBinding, IVM : IProjectViewModel?, VM : AbstractViewModel?>()
: Fragment() {
    abstract protected val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB

    protected val viewModel: IVM by viewModels<VM>()

    private var _binding: VB
    protected val binding
        get() = _binding

    override protected fun onCreateView(
        inflater: LayoutInflater,
        viewGroup: ViewGroup?,
        attachToRoot: Boolean 
    ) = bindingInflater(inflater, viewGroup, attachToRoot).also { _binding = it }.root
}


interface IFeatureViewModel: IProjectViewModel {

    // Declare your feature viewModel functions
}

@HiltViewModel
class FeatureViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    repository: IFeatureRepository
): AbstractViewModel(savedStateHandle), IFeatureViewModel {

    // Implement your feature viewModel functions
}


@AndroidEntryPoint
class FeatureFragment(): AbstractFragment<FragmentLayoutBinding, IFeatureViewModel, FeatureViewModel>() {

    override protected val bindingInflater = FragmentLayoutBinding::inflate
    
    override protected fun onViewCreated(...) {
        // Do your code magic because you should have access to both binding and viewModel here by now
        lifecycleScope.repeatOnLifecycle(STARTED) {
            viewModel?.uiState?.collectLatest {
                // You know the rest... binding.id. yada yada yada
            }
        }

        // Call your viewModel functions
    }
}
AndroidRocks
  • 292
  • 4
  • 16
-1

It's so simple to inject an interface, you pass an interface but the injection injects an Impl.

@InstallIn(ViewModelComponent::class)
@Module
class DIModule {

@Provides
fun providesRepository(): YourRepository = YourRepositoryImpl()

}
Amjad Alwareh
  • 2,926
  • 22
  • 27