13

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?
Hamza Sharaf
  • 811
  • 9
  • 25

3 Answers3

8

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.

Hamza Sharaf
  • 811
  • 9
  • 25
6

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

Linh
  • 57,942
  • 23
  • 262
  • 279
  • Which version of Hilt did you use? I am currently facing the same issue and your solution makes the jvm yield: lateinit property has not been initialized – Fattum Aug 09 '21 at 15:05
  • @Fattum, there is a github repo in the last of my answer, maybe you can check it, hope it help – Linh Aug 09 '21 at 15:27
  • I see. Thank you. I think i know where my Problem may be. I am trying to use your approach in the compose environment. I get my viewModel with a hiltViewModel() call. Perhaps, there is something wrong within the hilt-compose dependencies. – Fattum Aug 09 '21 at 16:33
  • That works fine, however, if I try to use the injected property `(api)` in the init block or to directly initialize a property it will throw the same `UninitializedPropertyAccessException`. Do you know what is the reason for that? why field injected property can't be used during the class or ViewModel initialization? – Hamza Sharaf Aug 18 '21 at 09:29
  • I just tested it again, it seems like field injection works well for the normal classes or activities, it only causes problems when it's used during ViewModel initialization. Still don't know why. – Hamza Sharaf Aug 18 '21 at 09:35
  • This is a common problem in the ViewModel field injection refer to my answer https://stackoverflow.com/a/68830188/11784905 It was unrelated to the question but thank you for your help – Hamza Sharaf Aug 18 '21 at 09:55
-1

After many searches on the Internet, I think the best solution is to not use initializer blocks init { ... } on the ViewModel, and instead create a function fun initialize() { ... } that will be called on the Fragment.

BaseViewModel.kt

@HiltViewModel
open class BaseViewModel @Inject constructor() : ViewModel() {
    @Inject
    protected lateinit var localUserRepository: LocalUserRepository
}

OnboardingViewModel.kt

@HiltViewModel
class OnboardingViewModel @Inject constructor() : BaseViewModel() {
    // Warning: don't use "init {}", the app will crash because of BaseViewModel
    // injected properties not initialized
    fun initialize() {
        if (localUserRepository.isLoggedIn()) {
            navigateToHomeScreen()
        }
    }
}

OnBoardingFragment.kt

@AndroidEntryPoint
class OnBoardingFragment() {

    override val viewModel: OnboardingViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.initialize()
    }
}

Sources:

romainb78
  • 51
  • 1
  • 8
  • 1
    It misses the whole point of using viewmodels, since this method will be called on every configuration change – Patroy Jan 17 '23 at 13:52