Dagger Hilt - How do I inject the ViewModel into the Adapter? - android

I am trying to inject the ViewModel into the adapter. It works fine while injecting into Fragment.
ViewModel:
class HomeViewModel #ViewModelInject constructor(
): ViewModel()
Fragment:
#AndroidEntryPoint
class HomeFragment : BaseFragment<FragmentHomeBinding, HomeViewModel>(
R.layout.fragment_home
) {
private val viewModel: HomeViewModel by viewModels()
There is no problem so far. But problems arise when I try to inject into the adapter.
class HomeListAdapter #Inject constructor(
): BaseListAdapter<Users>(
itemsSame = { old, new -> old.username == new.username },
contentsSame = { old, new -> old == new }
) {
private val viewModel: HomeViewModel by viewModels() //viewModels() unresolved reference
UPDATE:
If I try to use constructor injection or field injection I get the following error:
error: [Dagger/MissingBinding] ***.home.HomeViewModel cannot be provided without an #Inject constructor or an #Provides-annotated method.
public abstract static class ApplicationC implements App_GeneratedInjector,
^
***.home.HomeViewModel is injected at
***.home.adapter.HomeListAdapter.viewModel
***.home.adapter.HomeListAdapter is injected at
***.home.HomeFragment.viewAdapter
***.home.HomeFragment is injected at
***.home.HomeFragment_GeneratedInjector.injectHomeFragment(***.home.HomeFragment) [***.App_HiltComponents.ApplicationC → ***.App_HiltComponents.ActivityRetainedC → ***.App_HiltComponents.ActivityC → ***.App_HiltComponents.FragmentC]
Adapter:
class HomeListAdapter #Inject constructor(
): BaseListAdapter<Users>(
itemsSame = { old, new -> old.username == new.username },
contentsSame = { old, new -> old == new }
) {
#Inject lateinit var viewModel: HomeViewModel;

Generally, you should not inject ViewModel into Adapter, because Adapter is a part of presentation layer and an Android-specific thing. ViewModel is something independent on Android in general, though ViewModel from AAC is tied to it.
You should get your data in ViewModel and pass it to Fragment via LiveData, and then populate your Adapter from within Fragment.
by viewModels() is not defined in Adapter since it is an extension function of Fragment and could be used only within Fragment. So, move your ViewModel away from Adapter back to Fragment. That will also fix your compilation error since Hilt doesn't inject into Adapters.

Related

Way to share an dependency between class

I have three class needing to share a dependency. The latter is initialisated by one of them.The SettingsViewModel contains the data to initialize the dependency and it need to be deleted at the end of the activity. NetworkViewModel and TimeViewModel use it as an interface since the dependancy is an interface with the logic to handle Bluetooth.
SettingsViewModel -->(initialize) SingletonDependency.
NetworkViewModel --> (use).
TimeViewModel --> (use).
How can I make Hilt (or manual) injection to use the same interface? If I understand well I can't use singleton here since I need to iniatilize the dependency when the activity start.
If we consider that your class name is SomeClass you can provide a live data of this class like this:
#Module
#InstallIn(SingletonComponent::class)
object SingeltonModule {
#Provides
#Singleton
fun provideSomeClassLiveData(): MutableLiveData<SomeClass> {
return MutableLiveData<SomeClass>()
}
}
in your SettingsViewModel do this:
#HiltViewModel
class SettingsViewModel #Inject constructor(
val SomeClassLiveData: MutableLiveData<SomeClass>
) : ViewModel() {
init{
someClassLiveData.value = SomeClass()
}
}
and in other view models you can inject this to contractors and observe it:
#HiltViewModel
class NetworkViewModel #Inject constructor(
val SomeClassLiveData: MutableLiveData<SomeClass>
) : ViewModel() {
init{
someClassLiveData.observeForEver{
//do what you want with value
}
}
}

Hilt field injection in the super Fragment or ViewModel

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

Hilt java.lang.Integer cannot be provided without #inject constructor or #Provides - annotated method

Hello I'm building the app with hilt dependecy injector, but when I try to build the app i had error from the title. Here's my code:
Flow class:
#Entity(tableName = "flows_table")
data class Flow #Inject constructor(#PrimaryKey(autoGenerate = true) val id:Int, val name:String, val duration:Int, val actions:ArrayList<Action>)
Action class:
data class Action #Inject constructor(
val type: String,
val color: String?,
val brightness: Int?,
val duration: Float
)
My application:
#HiltAndroidApp
class YeebumApplication : Application() {
//get bulbs repository
private val bulbsDatabase by lazy { BulbsDatabase.getInstance(this)}
val bulbsRepository by lazy { BulbsRepository(bulbsDatabase!!.bulbsDao()) }
//get flows repository
private val flowsDatabase by lazy { FlowsDatabase.getInstance(this) }
val flowsRepository by lazy { FlowsRepository(flowsDatabase!!.flowsDao())}
}
My Fragment:
#AndroidEntryPoint
class ActionDetailsFragment : Fragment() {
#Inject
lateinit var flow: Flow
With the activity everything works perfectly, but when I try to inject dependiences in fragment android studio throw that exception.
When the Hilt annotation processor comes across the #Inject annotation in your fragment it is going to try and find (from a Hilt #Module with a #Provides annotated method) an instance of Flow.
I'm assuming from the error message you have not created this, so instead Hilt will move to the next option and try and create and instance of Flow itself.
So Hilt looks at the first property of Flow, id of type Int. And just like with the injection of Flow in your fragment, it repeats the process. It looks for an instance of Int from a Hilt #Module with a #Provides annotated method. This doesn't exist. Next option, try and construct it itself. Well Int is a platform type, you have no access, so you haven't created an Int class with an #Inject annotated constructor.
Hilt is now out of options and throws this Exception:
Hilt java.lang.Integer cannot be provided without #inject constructor or #Provides - annotated method

Hilt Inject into ViewModel without constructor params

With the new dependency injection library Hilt, how to inject some classes into ViewModel without constructor params and ViewModelFactory?
Is it possible?
Like in Fragment, we use only #AndroidEntryPoint and #Inject.
how to inject some classes into ViewModel without constructor params and ViewModelFactory? Is it possible?
Hilt supports constructor injection of ViewModel via the #HiltViewModel (previously #ViewModelInject) annotation.
This allows for any #AndroidEntryPoint-annotated class to redefine their defaultViewModelProviderFactory to be the HiltViewModelFactory, which allows the creation of #HiltViewModel-annotated ViewModels correctly instantiated via Dagger/Hilt.
NEW HILT VERSION:
#HiltViewModel
class RegistrationViewModel #Inject constructor(
private val someDependency: SomeDependency,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
...
}
OLD HILT VERSION:
class RegistrationViewModel #ViewModelInject constructor(
private val someDependency: SomeDependency,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
...
}
And then
#AndroidEntryPoint
class ProfileFragment: Fragment(R.layout.profile_fragment) {
private val viewModel by viewModels<RegistrationViewModel>() // <-- uses defaultViewModelProviderFactory
Yes, it is possible to inject dependency into a ViewModel class without constructor params. First we need to create a new interface annotated with #EntryPoint to access it.
An entry point is an interface with an accessor method for each
binding type we want (including its qualifier). Also, the interface
must be annotated with #InstallIn to specify the component in which to
install the entry point.
The best practice is adding the new entry point interface inside the class that uses it.
public class HomeViewModel extends ViewModel {
LiveData<List<MyEntity>> myListLiveData;
#ViewModelInject
public HomeViewModel(#ApplicationContext Context context) {
myListLiveData = getMyDao(context).getAllPosts();
}
public LiveData<List<MyEntity>> getAllEntities() {
return myListLiveData;
}
#InstallIn(ApplicationComponent.class)
#EntryPoint
interface MyDaoEntryPoint {
MyDao myDao();
}
private MyDao getMyDao(Context appConext) {
MyDaoEntryPoint hiltEntryPoint = EntryPointAccessors.fromApplication(
appConext,
MyDaoEntryPoint.class
);
return hiltEntryPoint.myDao();
}
}
In the code above we created a method named getMyDao and used EntryPointAccessors to retrieve MyDao from Application container.
Notice that the interface is annotated with the #EntryPoint and it's
installed in the ApplicationComponent since we want the dependency
from an instance of the Application container.
#Module
#InstallIn(ApplicationComponent.class)
public class DatabaseModule {
#Provides
public static MyDao provideMyDao(MyDatabase db) {
return db.MyDao();
}
}
Though the code above has been tested and worked properly but it is not the recommended way to inject dependency into ViewModel by android officials; and unless we know what we're doing, the best way is to inject dependency into ViewModel through constructor injection.

Inject Adapter class to Fragment using Dagger2

I have followed Android Architecture Blueprints Dagger2 for dependency injection: URL
Now I want to inject Adapter to my Fragment class:
#ActivityScoped
class MainFragment #Inject
constructor(): DaggerFragment(), ArtistClickCallback {
#Inject lateinit var adapter : ArtistAdapter
}
Main Module class:
#Module
abstract class MainModule {
#FragmentScoped
#ContributesAndroidInjector(modules = [MainFragmentModule::class])
internal abstract fun mainFragment(): MainFragment
#Binds
internal abstract fun bindArtistClickCallback(mainFragment: MainFragment) : ArtistClickCallback
}
MainFragmentModule:
#Module
class MainFragmentModule {
#Provides
fun provideArtistAdapter() = ArtistAdapter()
}
And this is my adapter class:
class ArtistAdapter #Inject constructor(
private val artistClickCallback : ArtistClickCallback
) : PagedListAdapter<LastFmArtist, RecyclerView.ViewHolder>(POST_COMPARATOR)
When I build the project I get following Kotlin compiler error:
error: [Dagger/DependencyCycle] Found a dependency cycle:
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.sample.android.lastfm.LastFmApp> {
^
com.sample.android.lastfm.ui.main.MainFragment is injected at
com.sample.android.lastfm.ui.main.MainModule.bindArtistClickCallback$app_debug(mainFragment)
com.sample.android.lastfm.ArtistClickCallback is injected at
com.sample.android.lastfm.ui.main.ArtistAdapter.artistClickCallback
com.sample.android.lastfm.ui.main.ArtistAdapter is injected at
com.sample.android.lastfm.ui.main.MainFragment.adapter
com.sample.android.lastfm.ui.main.MainFragment is injected at
com.sample.android.lastfm.ui.main.MainActivity.mainFragment
com.sample.android.lastfm.ui.main.MainActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.sample.android.lastfm.di.AppComponent → com.sample.android.lastfm.di.ActivityBindingModule_MainActivity$app_debug.MainActivitySubcomponent]
Can you suggest me how to solve this problem?
Codes can be found at URL
Your fragment should probably not have #ActivityScoped as a scope. Further do not use constructor injection with fragments (or any other framework type)! The Android framework will create those objects in some cases, and you will end up with the wrong reference in your classes. Add the fragment to the corresponding component via its builder.
Also you're using a provides annotated method as well as constructor injection (#Inject constructor()). Pick one. Since you also use field injection within the ArtistAdapter the next "error" you would encounter would be a null callback because you don't inject the adapter anywhere. You just create the object.
Constructor injection should usually be favored, which will also inject fields. Remove the following completely, keep the annotation on the construcor:
#Provides
fun provideArtistAdapter() = ArtistAdapter()
Moving on, your error originates in MainActivitySubcomponent (last line) and seems to be because your MainFragment is bound as an ArtistClickCallback, but requires a ArtistAdapter which requires a ArtistClickCallback...hence your dependency cycle.
This issue should resolve itself once you fix the problems mentioned (#Inject constructor on the fragment in this case) above, since it originates through the MainFragment being constructed by Dagger within the MainActivitySubcomponent, which is the wrong place anyways since your fragment should have a lower scope than the Activity.
Further you need to move your binding (#Binds fun bindArtistClickCallback) into the MainFragmentModule, since there is no fragment to bind in the Activity component (where you add the binding currently)
When you fix all those issues, you will bind your fragment to the correct FragmentSubcomponent, where you will bind it as a Callback, with which you can then create the Adapter and it should work.
I recommend you have a more thorough look on Dagger and make sure to understand all the issues / fixes pointed out.
This is how it should look
#FragmentScoped
class MainFragment(): DaggerFragment(), ArtistClickCallback {
#Inject lateinit var adapter : ArtistAdapter
}
#Module
abstract class MainModule {
#FragmentScoped
#ContributesAndroidInjector(modules = [MainFragmentModule::class])
internal abstract fun mainFragment(): MainFragment
}
#Module
class MainFragmentModule {
#Binds
internal abstract fun bindArtistClickCallback(mainFragment: MainFragment) : ArtistClickCallback
}
class ArtistAdapter #Inject constructor(
private val artistClickCallback : ArtistClickCallback
) : PagedListAdapter<LastFmArtist, RecyclerView.ViewHolder>(POST_COMPARATOR)

Categories

Resources