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?
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
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:
https://github.com/google/dagger/issues/2507
the answers on this question
Related
I'm quite new with Hilt injection. I started to migrate my whole project to DI.
It works almost everywhere, but I'm facing an issue when it comes to the leanback presenters. I don't know if it is related to the leanback stuff or juste Hilt
class LiveShowCardPresenter constructor(context: Context, listener: ShowCardViewListener, val hasVariableWidth: Boolean = false) : ShowCardPresenter(context, listener) {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val viewholder = ViewHolder(LiveShowCardView(context, hasVariableWidth))
viewholder.prepareViewHolderForeground(context, settings.isATV)
return viewholder
}
...
}
abstract class ShowCardPresenter constructor(val context: Context, var listener: ShowCardViewListener?) : Presenter() {
#Inject lateinit var detailsRepository: DetailsRepository
#Inject lateinit var settings: BackendSettings
... }
#Singleton
class BackendSettings #Inject constructor(#ApplicationContext val context: Context) {
val isATV = true // TODO
The following error occurs
kotlin.UninitializedPropertyAccessException: lateinit property settings has not been initialized
at ch.netplus.tv.ui.presenters.ShowCardPresenter.getSettings(ShowCardPresenter.kt:43)
at ch.netplus.tv.ui.presenters.LiveShowCardPresenter.onCreateViewHolder(LiveShowCardPresenter.kt:23)
It means it crashes when the settings.isATV is called because the 'settings' var is not initialized at that time. What should I do to have the injection done on time ?
Thanks !
How do you inject dependencies into the LiveShowCardPresenter?
Since your abstract class(ShowCardPresenter) performs field injection, you somehow need to inject these fields when you create LiveShowCardPresenter. To perform those injections, you need to inject LiveShowCardPresenter as well. So, here is how it will look:
class LiveShowCardPresenter #Inject constructor(context: Context) : ShowCardPresenter(context) {
var hasVariableWidth: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val viewholder = ViewHolder(LiveShowCardView(context, hasVariableWidth))
viewholder.prepareViewHolderForeground(context, settings.isATV)
return viewholder
}
...
}
abstract class ShowCardPresenter constructor(val context: Context) : Presenter() {
var listener: ShowCardViewListener? = null
#Inject lateinit var detailsRepository: DetailsRepository
#Inject lateinit var settings: BackendSettings
... }
YourFragment.kt
#AndroidEntryPoint
class YourFragment: BrowseFragment() {
#Inject
lateinit var liveShowCardPresenterProvider: Provider<LiveShowCardPresenter>
...
private void setupUIElements() {
...
//new header
setHeaderPresenterSelector(object : PresenterSelector() {
override fun getPresenter(o: Any): Presenter {
// Everytime when [liveShowCardPresenterProvider.get()] is called - new instance will be created
val presenter = liveShowCardPresenterProvider.get().apply {
// You can set your parameters here
// hasVariableWidth = true
// listener = yourCustomListener
}
return presenter;
}
});
}
...
If you need a single instance of the LiveShowCardPresenter in your fragment, you can perform a field injection on it without the Provider.
Alternativerly, you can inject all of your dependencies in the Fragment and pass them to the LiveShowCardPresenter constructor.
You need to set the inject method in BackendSettings
Like:
class BackendSettings #Inject constructor() {
}
Ok, I walk through your codes step by step:
Your LiveShowCardPresenter class should be changed as below:
class LiveShowCardPresenter #Inject constructor(
#ApplicationContext context: Context,
listener: ShowCardViewListener
) : ShowCardPresenter(context, listener) {
var hasVariableWidth = false
//your codes ...
}
As can be seen, #Inject is added before the constructor and also #ApplicationContext is added before context to provide context through the Hilt. also, hasVariableWidth is set outside of the constructor, if you don't like you can put it inside the constructor and provide it through the module and #Provide annotation. Now we should provide showCardViewListener. as I don't have access to your codes I provide it in a simple way.
#Module
#InstallIn(SingletonComponent::class)
abstract class ShowCardListenerModule {
#Binds
#Singleton
abstract fun bindShowCardViewListener(showCardViewListenerImpl: ShowCardViewListenerImpl) : ShowCardViewListener
}
ShowCardPresenter class has no changes. Finally, your BackendSettings class is changed like below:
#Singleton
class BackendSettings #Inject constructor() {
val isATV = true
//your codes ...
}
#Inject is added and #Singleton is removed because doesn't need to it. I ran the above codes and it works without any problem.
I have injected my repository in an Activity with lateinit declaration. However, when I am calling the method of repository it is resulting in a crash saying lateinit property clearDbRepository has not been initialized.
class StartActivity : BaseActivity() {
private lateinit var binding: StartEmptyPageBinding
#Inject
lateinit var clearDbRepository: ClearDbRepository
override fun setupViews() {
lifecycleScope.launch {
clearDbRepository.clearLocalDatabase()
}
}
}
My ClearDbRepository is:
#Singleton
class ClearDbRepository #Inject constructor(
private val mainDatabase: LocalDB
) {
suspend fun clearLocalDatabase() = withContext(Dispatchers.IO) {
mainDatabase.clearAllTables()
}
}
If you are using the Hilt library, then most probably, according to your code snippet, you're missing an annotation. You must add the appropriate annotation above your Activity class, like this:
#AndroidEntryPoint
class StartActivity : BaseActivity() { }
i am try to inject a module to MyViewModel
here is my Module
#Module
#InstallIn(ViewModelComponent::class)
object EngineModule {
#Provides
fun getEngine(): String = "F35 Engine"
}
and this my viewModel
#HiltViewModel
class MyViewModel #Inject constructor(): ViewModel() {
#Inject lateinit var getEngine: String
fun getEngineNameFromViewModel(): String = getEngineName()
}
and it throws
kotlin.UninitializedPropertyAccessException: lateinit property getEngine
has not been initialized
however if i change ViewModelComponent::class to ActivityComponent::class and inject like this
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#Inject
lateinit var getEngine: String
it works perfectly
any idea how to inject viewModels?
Also you can just remove #Inject constructor since you are already providing the dependency using dagger module:
#HiltViewModel
class MyViewModel (private val engineName: String): ViewModel() {
fun getEngineNameFromViewModel(): String = engineName
}
So, Basically you can either provide the dependency using dagger module or constructor injection.
Since required dependency is going to be injected in the ViewModel's constructor, you just need to modify your code in the following way to make it work:
#HiltViewModel
class MyViewModel #Inject constructor(private val engineName: String): ViewModel() {
fun getEngineNameFromViewModel(): String = engineName
}
I am facing this issue in multi module android project with HILT.
kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialized in MyViewModel
My modules are
App Module
Viewmodel module
UseCase Module
DataSource Module
'App Module'
#AndroidEntryPoint
class MainFragment : Fragment() {
private lateinit var viewModel: MainViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.test()
}}
'ViewModel Module'
class MainViewModel #ViewModelInject constructor(private val repository: MyUsecase): ViewModel() {
fun test(){
repository.test()
}}
'UseCase Module'
class MyUsecase #Inject constructor() {
#Inject
lateinit var feature: Feature
fun doThing() {
feature.doThing()
}
#Module
#InstallIn(ApplicationComponent::class)
object FeatureModule {
#Provides
fun feature(realFeature: RealFeature): Feature = realFeature
}
}
'DataSource Module'
interface Feature {
fun doThing()
}
class RealFeature : Feature {
override fun doThing() {
Log.v("Feature", "Doing the thing!")
}
}
Dependencies are
MyFragment ---> MyViewModel ---> MyUseCase ---> DataSource
what i did wrong with this code pls correct it.
above your activity class you must add annotation #AndroidEntryPoint
as below:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
In addition to moving all your stuff to constructor injection, your RealFeature isn't being injected, because you instantiate it manually rather than letting Dagger construct it for you. Note how your FeatureModule directly calls the constructor for RealFeature and returns it for the #Provides method. Dagger will use this object as is, since it thinks you've done all the setup for it. Field injection only works if you let Dagger construct it.
Your FeatureModule should look like this:
#Module
#InstallIn(ApplicationComponent::class)
object FeatureModule {
#Provides
fun feature(realFeature: RealFeature): Feature = realFeature
}
Or with the #Binds annotation:
#Module
#InstallIn(ApplicationComponent::class)
interface FeatureModule {
#Binds
fun feature(realFeature: RealFeature): Feature
}
This also highlights why you should move to constructor injection; with constructor injection, this mistake wouldn't have been possible.
The problem in the code is that #ViewModelInject doesn't work as #Inject in other classes. You cannot perform field injection in a ViewModel.
You should do:
class MainViewModel #ViewModelInject constructor(
private val myUseCase: MyUsecase
): ViewModel() {
fun test(){
myUseCase.test()
}
}
Consider following the same pattern for the MyUsecase class. Dependencies should be passed in in the constructor instead of being #Injected in the class body. This kind of defeats the purpose of dependency injection.
First, i think you are missing #Inject on your RealFeature class, so the Hilt knows how the inject the dependency. Second, if you want to inject into a class that is not a part of Hilt supported Entry points, you need to define your own entry point for that class.
In addition to the module that you wrote with #Provides method, you need to tell Hilt how the dependency can be accessed.
In your case you should try something like this:
#EntryPoint
#InstallIn(ApplicationComponent::class)
interface FeatureInterface {
fun getFeatureClass(): Feature
}
Then, when you want to use it, write something like this:
val featureInterface =
EntryPoints.get(appContext, FeatureInterface::class.java)
val realFeature = featureInterface.getFeatureClass()
You can find more info here:
https://dagger.dev/hilt/entry-points
https://developer.android.com/training/dependency-injection/hilt-android#not-supported
class MainViewModel #ViewModelInject constructor(private val repository: HomePageRepository,
#Assisted private val savedStateHandle: SavedStateHandle)
: ViewModel(){}
and intead of initializing the viewmodel like this :
private lateinit var viewModel: MainViewModel
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
Use this directly :
private val mainViewModel:MainViewModel by activityViewModels()
EXplanation :
assisted saved state handle : will make sure that if activity / fragment is annotated with #Android entry point combined with view model inject , it will automatically inject all required constructor dependencies available from corresonding component activity / application so that we won't have to pass these parameters while initializing viewmodel in fragment / activity
Make sure you added class path and plugin
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35'
in Project.gradle
apply plugin: 'dagger.hilt.android.plugin'
in app.gradle
I happen to have an Android lifecycle aware component with the following interface:
class MyLifecycleAwareComponent #Inject constructor(
private val: DependencyOne,
private val: DependencyTwo
) {
fun bindToLifecycleOwner(lifecycleOwner: LifecycleOwner) {
...
}
...
}
All Dagger specific components and modules are configured correctly and have been working great so far.
In each activity when I need to use the component I do the following:
class MyActivity: AppCompatActivity() {
#Inject
lateinit var component: MyLifecycleAwareComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
component.bindToLifecycleOwner(this)
...
}
}
Now I want to get rid of bindLifecycleOwner and denote my component like this:
class MyLifecycleAwareComponent #Inject constructor(
private val: DependencyOne,
private val: DependencyTwo,
private val: LifecycleOwner
) {
...
}
And provide the lifecycleOwner within the scope of individual activities (which implement the interface by extending AppCompatActivity).
Is there any way to do it with Dagger?
You may bind your Activity to LifecycleOwner from your ActivityModule:
#Module
abstract class ActivityModule {
...
#Binds
#ActivityScope
abstract fun bindLifecycleOwner(activity: AppCompatActivity): LifecycleOwner
...
}