Dagger: Can Hilt inject an Abstract class ViewModel?

Created on 7 Jul 2020  路  2Comments  路  Source: google/dagger

I am trying to inject abstract class into Fragment etc. in order to inject Fake object without mocking ViewModel with mockito etc. when doing Fragment test.

https://github.com/takahirom/hilt-viewmodel-abstract-inject/blob/master/app/src/main/java/com/github/takahirom/hilt/MainActivity.kt

abstract class MainViewModel : ViewModel() {
    abstract fun onClick()
}

class MainViewModelImpl @ViewModelInject constructor() : MainViewModel() {
    override fun onClick() {
        TODO("Not yet implemented")
    }
}

@InstallIn(ActivityRetainedComponent::class)
@Module
interface Module {
    @Binds
    fun provideMainViewModel(viewModel: MainViewModelImpl): MainViewModel
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    val viewModel by viewModels<MainViewModel>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println(viewModel)
    }
}

Currently doing this causes a crash.

2020-07-07 19:13:42.284 23496-23496/com.github.takahirom.hilt E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.github.takahirom.hilt, PID: 23496
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.github.takahirom.hilt/com.github.takahirom.hilt.MainActivity}: java.lang.RuntimeException: Cannot create an instance of class com.github.takahirom.hilt.MainViewModel
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3340)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3484)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2044)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7476)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:549)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:939)
     Caused by: java.lang.RuntimeException: Cannot create an instance of class com.github.takahirom.hilt.MainViewModel
        at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:221)
        at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:278)
        at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:106)
        at androidx.hilt.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:69)
        at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:69)
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:185)
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:54)
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)
        at com.github.takahirom.hilt.MainActivity.getViewModel(Unknown Source:2)
        at com.github.takahirom.hilt.MainActivity.onCreate(MainActivity.kt:37)
        at android.app.Activity.performCreate(Activity.java:7989)
        at android.app.Activity.performCreate(Activity.java:7978)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3315)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3484)聽
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)聽
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)聽
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)聽
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2044)聽
        at android.os.Handler.dispatchMessage(Handler.java:106)聽
        at android.os.Looper.loop(Looper.java:223)聽
        at android.app.ActivityThread.main(ActivityThread.java:7476)聽
        at java.lang.reflect.Method.invoke(Native Method)聽
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:549)聽
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:939)聽
     Caused by: java.lang.InstantiationException: java.lang.Class<com.github.takahirom.hilt.MainViewModel> cannot be instantiated
        at java.lang.Class.newInstance(Native Method)
        at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:219)
        at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:278)聽
        at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:106)聽
        at androidx.hilt.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:69)聽
        at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:69)聽
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:185)聽
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)聽
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:54)聽
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)聽
        at com.github.takahirom.hilt.MainActivity.getViewModel(Unknown Source:2)聽
        at com.github.takahirom.hilt.MainActivity.onCreate(MainActivity.kt:37)聽
        at android.app.Activity.performCreate(Activity.java:7989)聽
        at android.app.Activity.performCreate(Activity.java:7978)聽
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)聽
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3315)聽
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3484)聽
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)聽
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)聽
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)聽
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2044)聽
        at android.os.Handler.dispatchMessage(Handler.java:106)聽
        at android.os.Looper.loop(Looper.java:223)聽
        at android.app.ActivityThread.main(ActivityThread.java:7476)
hilt

Most helpful comment

This is not easily supported right now, but you can make it work if you change your binds module to:

@InstallIn(ActivityRetainedComponent::class)
@Module
interface Module {
    @Binds
    @IntoMap
    @StringKey("com.github.takahirom.hilt.MainViewModel")
    fun bind(factory: MainViewModelImpl_AssistedFactory): ViewModelAssistedFactory<out ViewModel>
}

while also enabling KAPT corrected error types:

// app's build.gradle
dependencies {
  ...
}

kapt {
  correctErrorTypes = true
}

Hilt ViewModel extension works by declaring modules that bind assisted factories to a map and not by binding concrete ViewModels. Therefore, what you want to do is bind the assisted factory of the concrete ViewModel using the key of the abstract ViewModel so that when HiltViewModelFactory looks up the factory based on class key it uses the assisted factory for the concrete ViewModel. This is suuuper obscure and hence why I mean not 'easily' available.

However, if you can expand on the test case your are trying to write that could help us provide some guidance, I'm not sure if you are trying to mock/fake the ViewModel itself for tests, but Hilt testing APIs should allow you to replace dependencies in the ViewModel so you can write a test with the Fragment and the ViewModel.

All 2 comments

This is not easily supported right now, but you can make it work if you change your binds module to:

@InstallIn(ActivityRetainedComponent::class)
@Module
interface Module {
    @Binds
    @IntoMap
    @StringKey("com.github.takahirom.hilt.MainViewModel")
    fun bind(factory: MainViewModelImpl_AssistedFactory): ViewModelAssistedFactory<out ViewModel>
}

while also enabling KAPT corrected error types:

// app's build.gradle
dependencies {
  ...
}

kapt {
  correctErrorTypes = true
}

Hilt ViewModel extension works by declaring modules that bind assisted factories to a map and not by binding concrete ViewModels. Therefore, what you want to do is bind the assisted factory of the concrete ViewModel using the key of the abstract ViewModel so that when HiltViewModelFactory looks up the factory based on class key it uses the assisted factory for the concrete ViewModel. This is suuuper obscure and hence why I mean not 'easily' available.

However, if you can expand on the test case your are trying to write that could help us provide some guidance, I'm not sure if you are trying to mock/fake the ViewModel itself for tests, but Hilt testing APIs should allow you to replace dependencies in the ViewModel so you can write a test with the Fragment and the ViewModel.

Hello Daniel
Thank you for thinking.
The following are shared as personal use cases.
Actually I'm trying to test how it's displayed by a ViewModel property (UiModel) by comparing screenshots etc. So I want to mock the ViewModel.
I will try to find another approach.

class MainViewModel @ViewModelInject constructor() : ViewModel() {
    class UiModel(
        val isShowProgress: Boolean
    )

    private val mutableUiModel = MutableLiveData(UiModel(true))
    val uiModel: LiveData<UiModel>
        get() = mutableUiModel

    fun onClick() {
        TODO("Not yet implemented")
    }
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    val viewModel by viewModels<MainViewModel>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel.uiModel.observe(this) { uiModel ->
//            binding.progressBar.isVisible = uiModel.isShowProgress
        }
    }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

gc986 picture gc986  路  4Comments

HiroyukTamura picture HiroyukTamura  路  3Comments

makaroffandrey picture makaroffandrey  路  3Comments

pyricau picture pyricau  路  4Comments

blackberry2016 picture blackberry2016  路  3Comments