Hilt - How to inject ViewModel interface? - android

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?

viewModels requires child of ViewModel class
val viewModel: ExampleViewModel by viewModels<ExampleViewModelImp>()

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.
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()
I don't think there is a way to use #HiltViewModel for this purpose, since there was no way to switch the implementation.
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

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

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()
}

Related

Send intent arguments from bundle to ViewModel using Hilt

I'm trying to send three values to my UserViewModel but even if sending the savedStateHandle
In my Activity I have
private val viewModel: UserViewModel by viewModels()
Then my UserViewModel is :
#HiltViewModel
internal class UserViewModel #Inject constructor(
private val myRepo: MyRepo,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
But then this savedStateHandle is empty, what I'm missing?
If you are using MVVM based on the Android Architecture guidelines you can send an event to the viewmodel from your Activity/Fragment once your view is created.
You must add savedStateHandle in AppModule. You want inject savedStateHandle.
I've been using #AssistedInject to do so as follows :
internal class UserViewModel #AssistedInject constructor(
...
#Assisted val name: String,
) : ViewModel() {
...
}
Then I had to create a Factory
#AssistedFactory
interface UserViewModelAssistedFactory {
fun create(name: String): UserViewModel
}
class Factory(
private val assistedFactory: UserViewModelAssistedFactory,
private val name: String, <-- value you want to pass
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(name) as T
}
}
Then in the Activity/Fragment you have to inject the AssistedFactory as follows
#Inject internal lateinit var assistedFactory: UserViewModel.UserViewModelAssistedFactory
private val userViewModel: UserViewModel by viewModels {
UserViewModel.Factory(assistedFactory, intent.getStringExtra(USER_NAME_ARG).orEmpty())
}
Doing this it should work, but also your solution should work make sure you are sending the intent args correctly because it says is null looks like what you are passing is not correct, savedInstace.keys() should return everything you passed from your previous Activity/Fragment.

Cannot create an instance of class ViewModel using dagger hilt

My ViewModel:
class LoginViewModel #ViewModelInject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
val currentResult: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
fun loginUseCase(username: String, password: String) {
viewModelScope.launch {
loginUseCase.invoke(username, password).apiKey.let {
currentResult.value = it
}
}
}
}
Is being used by my MainActivity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val loginViewModel: LoginViewModel by viewModels()
And I know that the ViewModelProvider is expecting a empty constructor but I need to use the LoginUseCase:
class LoginUseCase #Inject constructor(
private val apiService: ApiServiceImpl
) : UseCase<Unit>() {
suspend operator fun invoke(username: String, password: String) =
apiService.login(username, password)
}
Inside the modelView, but i get the error:
Cannot create an instance of class com.example.myboards.ui.login.LoginViewModel
in runtime, and I dont know how I could manage the LoginUseCase inside the LoginViewModel
Provide a ViewModel by annotating it with #HiltViewModel and using the #Inject annotation in the ViewModel object's constructor.
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
...
}
Hilt needs to know how to provide instances of ApiServiceImpl, too. Read here to know how to inject interface instances with #Binds.
Let me know If you still have a problem.

Dagger2 inject specific instance of abstract ViewModel

I have a ViewModelFactory implemented as follows:
class ViewModelFactory<VM> #Inject constructor(private val viewModel: Lazy<VM>)
: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
#Suppress("UNCHECKED_CAST")
return viewModel.get() as T
}
}
This works fine with my current ViewModel:
class MainActivityViewModel #Inject constructor(private val dependency: Dependency) : ViewModel()
//... in the activity:
#Inject
lateinit var factory: ViewModelFactory<MainActivityViewModel>
private val viewModel: MainActivityViewModel by viewModels { factory }
However I have a different build flavour that I want to implement where the behaviour is different, so I have created an AbstractViewModel:
abstract class AbstractViewModel : ViewModel()
//...and so now
class MainActivityViewModel #Inject constructor(private val dependency: Dependency) : AbstractViewModel()
//... and in the activity
#Inject
lateinit var factory: ViewModelFactory<AbstractViewModel>
private val viewModel: AbstractViewModel by viewModels { factory }
I want to be able to provide the specific instance to the ViewModelFactory, but I am not sure how to achieve this.
Solved it. Answering for future reference and anyone who may be interested in doing something similar.
Step 1: Add a new module component
#Component(
modules = [
//...
ViewModelModule::class
]
)
interface ApplicationComponent { //...
This allows developers to create a FlavourApplicationComponent that can have an alternate to ViewModelModule, which I called MockViewModelModule
Step 2: Define the modules
//in the main flavour
#Module
class ViewModelModule {
#Provides
fun provideMainActivityViewModel(mainActivityViewModel: MainActivityViewModel): AbstractViewModel
= mainActivityViewModel
}
//in the mock flavour
#Module
class MockViewModelModule {
#Provides
fun provideMainActivityViewModel(mainActivityViewModel: MainActivityViewModel): AbstractViewModel
= MockMainActivityViewModel()
}
And this can actually be configured at run time, and therefore you can allow users to test all the different states of your app without them needing to GET into those states.

How to use AssistedInject to pass dynamic value as a parameter to ViewModel when using Dagger2 in Android

I am new to Dagger 2 in android. I am having trouble understanding how to inject ViewModel with dynamic value. So Far I have successfully injected ViewModel using dagger multi binding with pre-defined repository dependency. Here's my code.
ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
FragmentModule
#Module
abstract class FragmentModule {
#Binds
#IntoMap
#ViewModelKey(WeatherViewModel::class)
abstract fun bindWeatherView(weatherViewModel: WeatherViewModel) : ViewModel
}
ViewModelFactoryModule
#Module
class ViewModelFactoryModule {
#Provides
#Singleton
fun viewModelFactory(providerMap: Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(providerMap)
}
}
Application class
class ThisApplication: Application(),InjectorProvider {
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
override val component by lazy {
DaggerApplicationComponent.factory().create(applicationContext)
}
}
I'm using InjectorProvider interface to get dagger to fragments and activity without having to cast every time.
InjectorProvider
interface InjectorProvider {
val component: ApplicationComponent
}
val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component
This is the simple ViewModel I used for testing ViewModel injection.
WeatherViewModel
class WeatherViewModel #Inject constructor(val repository: WeatherRepository): ViewModel() {
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Finally, I Injected this view model into a fragment like below.
WeatherFragment
class WeatherFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onAttach(context: Context) {
injector.fragmentComponent().create().injectWeatherFragment(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mainActivityViewModel =
ViewModelProvider(this,viewModelFactory)[WeatherViewModel::class.java]
mainActivityViewModel.printMessage()
}
}
This part is working fine. The Log message inside printMessage() getting printed. I saw in the dagger issue discussion that using AssistedInject is the best approach to handle this kind of scenario. I changed my ViewModle by adding a simple int value as a parameter.
Edited WeatherViewModel
class WeatherViewModel #AssistedInject constructor(val repository: WeatherRepository,
#Assisted val id: Int): ViewModel() {
#AssistedInject.Factory
interface Factory{ fun create(id: Int) : WeatherViewModel }
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Edited ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class, AssistedInjectModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
#AssistedModule
#Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule
From this point onwards I don't understand how to inject ViewModel into fragment with repository plus dynamic "id" value. If I inject WeatherViewModel.Factory into the fragment by calling the create method (val mainActivityViewModel = factory.create(5)) it won't fulfill the repository dependency in ViewModel. How to combine these two solutions to have pre-defined repository dependency with dynamic value? OR is there any other better way of approaching this?
Not quite sure why your setup wont fulfill repository dependency by using create() method of factory. The repository dependency will be provided by Dagger's Acyclic Dependency Graph.
For example, below I'm saying to Dagger that I am responsible for providing SavedStateHandle and the NavigationDispatcher so don't even bother looking these up in your acyclic dependency graph.
class ProfileViewModel #AssistedInject constructor(
#Assisted val handle: SavedStateHandle,
#Assisted val navigationDispatcher: NavigationDispatcher,
private val eventTracker: EventTracker,
private val getUserUseCase: GetUserUseCase,
private val logOutUseCase: LogOutUseCase
) : ViewModel(), ProfileHandler {
#AssistedInject.Factory
interface Factory {
fun create(
handle: SavedStateHandle,
navigationDispatcher: NavigationDispatcher
): ProfileViewModel
}
In Fragment side, all I have to provide in the create method will be the dependencies i marked with #Assisted to fulfil my side of promise.
class ProfileFragment : Fragment() {
private val navigationDispatcher by getActivityViewModel {
getBaseComponent().navigationDispatcher
}
private val eventTracker by lazy {
getProfileComponent().eventTracker
}
private val viewModel by getViewModel { savedStateHandle ->
getProfileComponent().profileViewModelFactory.create(savedStateHandle, navigationDispatcher)
}
getViewModel is simply an extension function as follows:
inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline provider: (handle: SavedStateHandle) -> T) =
viewModels<T> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = provider(handle) as T
}
}

Use the same instance of view model in multiple fragments using dagger2

I am using only dagger2 (not dagger-android) in my project. It's working fine to inject the ViewModel using multibinding. But there's one problem with that previously without dagger2 I was using the same instance of viewmodel used in activity in multiple fragments (using fragment-ktx method activityViewModels()), but now since dagger2 is injecting the view model it's always gives the new instance (checked with hashCode in each fragment) of the viewmodel for each fragment, that's just breaks the communication between fragment using viewmodel.
The fragment & viewmodel code is as below:
class MyFragment: Fragment() {
#Inject lateinit var chartViewModel: ChartViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
(activity?.application as MyApp).appComponent.inject(this)
}
}
//-----ChartViewModel class-----
class ChartViewModel #Inject constructor(private val repository: ChartRepository) : BaseViewModel() {
//live data code...
}
Here's the code for viewmodel dependency injection:
//-----ViewModelKey class-----
#MapKey
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
//-----ViewModelFactory class------
#Singleton
#Suppress("UNCHECKED_CAST")
class ViewModelFactory
#Inject constructor(
private val viewModelMap: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModelMap[modelClass] ?: viewModelMap.asIterable()
.firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("Unknown ViewModel class $modelClass")
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
//-----ViewModelModule class-----
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(ChartViewModel::class)
abstract fun bindChartViewModel(chartViewModel: ChartViewModel): ViewModel
}
Is there any way to achieve the same instance of viewmodel for multiple fragment and also at the same time inject the view model in fragments.
Also is there any need for the bindViewModelFactory method as it seems to have no effect on app even without this method.
One workaround could be to make a BaseFragment for fragments which shares the common viewmodel, but that will again include the boilerplate code and also I am not a great fan of BaseFragment/BaseActivity.
This is generated code for ChartViewModel which always create the newInstance of viewModel:
#SuppressWarnings({
"unchecked",
"rawtypes"
})
public final class ChartViewModel_Factory implements Factory<ChartViewModel> {
private final Provider<ChartRepository> repositoryProvider;
public ChartViewModel_Factory(Provider<ChartRepository> repositoryProvider) {
this.repositoryProvider = repositoryProvider;
}
#Override
public ChartViewModel get() {
return newInstance(repositoryProvider.get());
}
public static ChartViewModel_Factory create(Provider<ChartRepository> repositoryProvider) {
return new ChartViewModel_Factory(repositoryProvider);
}
public static ChartViewModel newInstance(ChartRepository repository) {
return new ChartViewModel(repository);
}
}
The problem is that when you inject the viewmodel like this
class MyFragment: Fragment() {
#Inject lateinit var chartViewModel: ChartViewModel
dagger simply creates a new viewmodel instance. There is no viewmodel-fragment-lifecycle magic going on because this viewmodel is not in the viewmodelstore of the activity/fragment and is not being provided by the viewmodelfactory you created. Here, you can think of the viewmodel as any normal class really. As an example:
class MyFragment: Fragment() {
#Inject lateinit var anything: AnyClass
}
class AnyClass #Inject constructor(private val repository: ChartRepository) {
//live data code...
}
Your viewmodel is equivalent to this AnyClass because the viewmodel is not in the viewmodelstore and not scoped to the lifecycle of the fragment/activity.
Is there any way to achieve the same instance of viewmodel for multiple fragment and also at the same time inject the view model in fragments
No. Because of the reasons listed above.
Also is there any need for the bindViewModelFactory method as it seems to have no effect on app even without this method.
It does not have any effect because (I'm assuming that) you are not using the ViewModelFactory anywhere. Since it's not referenced anywhere, this dagger code for the viewmodelfactory is useless.
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
Here's what #binds is doing: 1 2
That's why removing it has no effect on the app.
So what is the solution? You need to inject the factory into the fragment/activity and get the instance of the viewmodel using the factory
class MyFragment: Fragment() {
#Inject lateinit var viewModelFactory: ViewModelFactory
private val vm: ChartViewModel by lazy {
ViewModelProvider(X, YourViewModelFactory).get(ChartViewModel::class.java)
}
What is X here? X is ViewModelStoreOwner. A ViewModelStoreOwner is something that has viewmodels under them. ViewModelStoreOwner is implemented by activity and fragment. So you have a few ways of creating a viewmodel:
viewmodel in activity
ViewModelProvider(this, YourViewModelFactory)
viewmodel in fragment
ViewModelProvider(this, YourViewModelFactory)
viewmodel in fragment (B) scoped to a parent fragment (A) and shared across child fragments under A
ViewModelProvider(requireParentFragment(), YourViewModelFactory)
viewmodel in fragment scoped to parent activity and shared across fragments under the activity
ViewModelProvider(requireActivity(), YourViewModelFactory)
One workaround could be to make a BaseFragment for fragments which shares the common viewmodel, but that will again include the boilerplate code and also I am not a great fan of BaseFragment/BaseActivity
Yes, this is indeed a bad idea. The solution is to use requireParentFragment() and requireActivity() to get the viewmodel instance. But you'll be writing the same in every fragment/activity that has a viewmodel. To avoid that you can abstract away this ViewModelProvider(x, factory) part in a base fragment/activity class and also inject the factory in the base classes, which will simplify your child fragment/activity code like this:
class MyFragment: BaseFragment() {
private val vm: ChartViewModel by bindViewModel() // or bindParentFragmentViewModel() or bindActivityViewModel()
You can share ViewModel between fragments when instantiating if the fragments has the same parent activity
FragmentOne
class FragmentOne: Fragment() {
private lateinit var viewmodel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewmodel= activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} : throw Exception("Invalid Activity")
}
}
FragmentTwo
class FragmentTwo: Fragment() {
private lateinit var viewmodel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewmodel= activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
}
Add your ViewModel as PostListViewModel inside ViewModelModule:
#Singleton
class ViewModelFactory #Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
}
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(PostListViewModel::class)
internal abstract fun postListViewModel(viewModel: PostListViewModel): ViewModel
//Add more ViewModels here
}
To end with, our activity will have ViewModelProvider.Factory injected and it will be passed to theprivate val viewModel: PostListViewModel by viewModels { viewModelFactory }
class PostListActivity : AppCompatActivity() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel: PostListViewModel by viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post_list)
getAppInjector().inject(this)
viewModel.posts.observe(this, Observer(::updatePosts))
}
//...
}
For more check this post:Inject ViewModel with Dagger2 And Check github

Categories

Resources