Hilt field injection in the super Fragment or ViewModel

Viewed 3513

I'm using Dagger-Hilt for dependency injection in my Android project, now I have this situation where I have a base abstract Fragment

BaseViewModel.kt

abstract class BaseViewModel constructor(
    val api: FakeApi,
) : ViewModel() {
    
    //...
    
}

Here, I have a dependency which is FakeApi. What I'm trying to do is to inject the FakeApi into the BaseViewModel to be available in the BaseViewModel and all its children.

  • The first approach I tried is using the constructor injection and inject it to the child and pass it to the super using the constructor.

TaskViewModel.kt

@HiltViewModel
class TaskViewModel @Inject constructor(
    api: FakeApi
) : BaseViewModel(api){

}

This approach works fine, but I don't need to pass the dependency from the child to the super class, I need the FakeApi to be automatically injected in the BaseViewModel without having to pass it as I have three levels of abstraction (There is another class inheriting from the TaskViewModel) So I have to pass it two times.

  • The second approach was to use the field injection as follows

BaseViewModel.kt

abstract class BaseViewModel: ViewModel() {
    @Inject
    lateinit var api: FakeApi
    //...
}

TaskViewModel.kt

@HiltViewModel
class TaskViewModel @Inject constructor(): BaseViewModel() {
    
}

This approach didn't work for me and the FakeApi wasn't injected and I've got an Exception

kotlin.UninitializedPropertyAccessException: lateinit property api has not been initialized

My questions are

  • Why field injection doesn't work for me?
  • Is there any way to use constructor injection for the super class instead of passing the dependency from the child?
2 Answers

Thanks to this Github Issue I figured out that the problem is that you can't use the field injected properties during the ViewModel constructor initialization, but you still use it after the constructor -including all the properties direct initialization- has been initialized.

Dagger firstly completes the constructor injection process then the field injection process takes place. that's why you can't use the field injection before the constructor injection is completed.

❌ Wrong use

abstract class BaseViewModel : ViewModel() {

    @Inject
    protected lateinit var fakeApi: FakeApi

    val temp = fakeApi.doSomething() // Don't use it in direct property declaration

    init {
        fakeApi.doSomething() // Don't use it in the init block
    }
}

✔️ Right use

abstract class BaseViewModel : ViewModel() {

    @Inject
    protected lateinit var fakeApi: FakeApi

    val temp: Any
        get() = fakeApi.doSomething() // Use property getter

    fun doSomething(){
        fakeApi.doSomething() // Use it after constructor initialization
    }
}

Or you can use the by lazy to declare your properties.

I tested and I see that field injection in base class still work with Hilt 2.35. I can not get the error like you so maybe you can try to change the Hilt version or check how you provide FakeApi

abstract class BaseViewModel : ViewModel() {

    @Inject
    protected lateinit var fakeApi: FakeApi
}

FakeApi

// Inject constructor also working
class FakeApi {

    fun doSomeThing() {
        Log.i("TAG", "do something")
    }
}

MainViewModel

@HiltViewModel
class MainViewModel @Inject constructor() : BaseViewModel() {

    // from activity, when I call this function, the logcat print normally 
    fun doSomeThing() {
        fakeApi.doSomeThing()
    }
}

AppModule

@Module
@InstallIn(SingletonComponent::class)
class AppModule {

    @Provides
    fun provideAPI(
    ): FakeApi {
        return FakeApi()
    }
}

https://github.com/PhanVanLinh/AndroidHiltInjectInBaseClass

Related