class MovieListFragment : Fragment() {
#Inject
lateinit var movieListView: MovieListViewModel
private lateinit var movieListAdapter: MovieListAdapter
private lateinit var binding: ListFragmentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerMovieComponent.builder().appComponent(MovieListApp.component()).fragmentModule(FragmentModule(this)).build().inject(this)
}
This is the class I'm trying to have my viewmodel injected.
#Module (includes = [FragmentModule::class])
class MovieListModule(fragment: Fragment) {
private lateinit var movieListView : MovieListViewModel
#Provides
fun getMovieListViewModel(fragment: Fragment): MovieListViewModel {
movieListView = ViewModelProvider(fragment).get(MovieListViewModel::class.java)
return movieListView
}
}
This is the class that has the module and lastly,
#Singleton
#Component(modules = [MovieModule::class,MovieListModule::class], dependencies = [AppComponent::class]))
interface MovieComponent {
fun inject(movieListViewModel : MovieListViewModel)
fun inject(movieDetailViewModel: MovieDetailViewModel)
fun inject(fragment : Fragment)
}
This is my component interface.
The app crashes, saying that the lateinit viewmodel that was supposed to be injected is not initialised. Is there a way around this?
Thank you in advance.
The error message:
2022-03-30 15:41:40.749 18607-18607/com.example.polyapp E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.polyapp, PID: 18607
java.lang.RuntimeException: Unable to create application com.example.polyapp.MovieListApp: java.lang.IllegalStateException: com.example.polyapp.movieDatabaseFeature.di.AppComponent must be set
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7487)
at android.app.ActivityThread.access$1700(ActivityThread.java:310)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2283)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8641)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133)
Caused by: java.lang.IllegalStateException: com.example.polyapp.movieDatabaseFeature.di.AppComponent must be set
at dagger.internal.Preconditions.checkBuilderRequirement(Preconditions.java:95)
at com.example.polyapp.movieDatabaseFeature.di.DaggerMovieComponent$Builder.build(DaggerMovieComponent.java:101)
at com.example.polyapp.MovieListApp.onCreate(MovieListApp.kt:15)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1211)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7482)
at android.app.ActivityThread.access$1700(ActivityThread.java:310)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2283)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8641)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133)
ViewModel injection on Android is tricky because ViewModels are created using ViewModelProvider to ensure they survive configuration changes. If they're created with ViewModelProvider, then how do you create it with Dagger? Luckily they both provide API's that can mesh together to solve your problem.
Dagger has Multibindings, and ViewModelProvider has it's ViewModelProvider.Factory API. Multibindings allow us to more finely tune when injection occurs by looking it up on a map first. The ViewModelProvider.Factory will tell the ViewModelProvider how to construct your ViewModel which allows for you to specify constructor parameters.
Here are the steps and explanations:
Annotate your MovieListViewModel constructor with #Inject. This will tell Dagger to put your MovieListViewModel on its' graph provided it can satisfy the constructor parameters(we won't be injecting it directly, we just need it on the Dagger graph). If there are no parameters, Dagger will handle it just fine.
import javax.inject.Inject
class MovieListViewModel #Inject constructor() {
...
}
Create a Multibinding for your MovieListViewModel. Instead of directly injecting your MovieListViewModel into the fragment, we want to wrap it in a special Dagger feature called a Multibinding. This will allow you to put your MovieListViewModel into a Map which can be injected and more finely manipulated at runtime(remember that ViewModelProvider.Factory I mentioned?).
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
annotation class ViewModelKey(val value: KClass<out ViewModel>)
#Module
abstract class MovieListViewModelMultiBinder
{
#Binds // Tells Dagger to use the parameter value here, which extends ViewModel
#IntoMap // Tells Dagger to put this ViewModel implementation into a map. This requires you to provide a key which is known at compile time.
#ViewModelKey(MovieListViewModel::class) // Tells Dagger the key to use for this ViewModel. This is your ViewModel class.
// The parameter is the implementation to use when we request a `ViewModel`. Since this is a multibinding, multiple ViewModels can be bound.
fun bind(viewModel: MovieListViewModel): ViewModel
}
Create a ViewModelProvider.Factory that injects and uses the Map mentioned in step 2. This uses a special Dagger type called a Provider. Providers wrap your injected type and do not construct it until you call Provider.get() to retrieve your object.
class DaggerViewModelFactory #Inject constructor(
private val viewModelProviders: Map<Class<out ViewModel>, Provider<ViewModel>>
): ViewModelProvider.Factory
{
override fun <T: ViewModel?> create(modelClass: Class<T>): T
{
return viewModelProviders[modelClass]?.get() as T
}
}
Use your custom ViewModelProvider.Factory in your Fragments and Activities.
class MyActivity: Activity() {
private lateinit var viewModel: MovieListViewModel
#Inject lateinit var factory: DaggerViewModelFactory
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(MovieListViewModel::class.java)
}
}
For the sake of simplicity, I did not throw an error if the DaggerViewModelFactory returns null for the modelClass, but you should add one in case you forget to bind your ViewModel into the Multibinding.
Hope this helps.
Related
I'm trying to wrap my head around Hilt and the way it deals with ViewModels.
I would like my fragments to depend on abstract view models, so I can easily mock them during UI tests. Ex:
#AndroidEntryPoint
class MainFragment : Fragment() {
private val vm : AbsViewModel by viewModels()
/*
...
*/
}
#HiltViewModel
class MainViewModel(private val dependency: DependencyInterface) : AbsViewModel()
abstract class AbsViewModel : ViewModel()
Is there a way to configure by viewModels() so that it can map concrete implementations to abstract view models? Or pass a custom factory producer to viewModels() that can map concrete view models instances to abstract classes?
The exact question is also available here, but it is quite old considering hilt was still in alpha then: https://github.com/google/dagger/issues/1972
However, the solution provided there is not very desirable since it uses a string that points to the path of the concrete view model. I think this will not survive obfuscation or moving files and it can quickly become a nightmare to maintain. The answer also suggests injecting a concrete view model into the fragment during tests with all the view model's dependencies mocked, thus gaining the ability to control what happens in the test. This automatically makes my UI test depend on the implementation of said view model, which I would very much like to avoid.
Not being able to use abstract view models in my fragments makes me think I'm breaking the D in SOLID principles, which is something that I would also like to avoid.
Not the cleanest solution, but here's what I managed to do.
First create a ViewModelClassesMapper to help map an abstract class to a concrete one. I'm using a custom AbsViewModel in my case, but this can be swapped out for the regular ViewModel. Then create a custom view model provider that depends on the above mapper.
class VMClassMapper #Inject constructor (private val vmClassesMap: MutableMap<Class<out AbsViewModel>, Provider<KClass<out AbsViewModel>>>) : VMClassMapperInterface {
#Suppress("TYPE_INFERENCE_ONLY_INPUT_TYPES_WARNING")
override fun getConcreteVMClass(vmClass: Class<out AbsViewModel>): KClass<out AbsViewModel> {
return vmClassesMap[vmClass]?.get() ?: throw Exception("Concrete implementation for ${vmClass.canonicalName} not found! Provide one by using the #ViewModelKey")
}
}
interface VMClassMapperInterface {
fun getConcreteVMClass(vmClass: Class<out AbsViewModel>) : KClass<out AbsViewModel>
}
interface VMDependant<VM : AbsViewModel> : ViewModelStoreOwner {
fun getVMClass() : KClass<VM>
}
class VMProvider #Inject constructor(private val vmMapper: VMClassMapperInterface) : VMProviderInterface {
#Suppress("UNCHECKED_CAST")
override fun <VM : AbsViewModel> provideVM(dependant: VMDependant<VM>): VM {
val concreteClass = vmMapper.getConcreteVMClass(dependant.getVMClass().java)
return ViewModelProvider(dependant).get(concreteClass.java) as VM
}
}
interface VMProviderInterface {
fun <VM :AbsViewModel> provideVM(dependant: VMDependant<VM>) : VM
}
#Module
#InstallIn(SingletonComponent::class)
abstract class ViewModelProviderModule {
#Binds
abstract fun bindViewModelClassesMapper(mapper: VMClassMapper) : VMClassMapperInterface
#Binds
#Singleton
abstract fun bindVMProvider(provider: VMProvider) : VMProviderInterface
}
Then, map your concrete classes using the custom ViewModelKey annotation.
#Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey(val value: KClass<out AbsViewModel>)
#Module
#InstallIn(SingletonComponent::class)
abstract class ViewModelsDI {
companion object {
#Provides
#IntoMap
#ViewModelKey(MainContracts.VM::class)
fun provideConcreteClassForMainVM() : KClass<out AbsViewModel> = MainViewModel::class
#Provides
#IntoMap
#ViewModelKey(SecondContracts.VM::class)
fun provideConcreteClassForSecondVM() : KClass<out AbsViewModel> = SecondViewModel::class
}
}
interface MainContracts {
abstract class VM : AbsViewModel() {
abstract val textLiveData : LiveData<String>
abstract fun onUpdateTextClicked()
abstract fun onPerformActionClicked()
}
}
interface SecondContracts {
abstract class VM : AbsViewModel()
}
Finally, your fragment using the abstract view model looks like this:
#AndroidEntryPoint
class MainFragment : Fragment(), VMDependant<MainContracts.VM> {
#Inject lateinit var vmProvider: VMProviderInterface
protected lateinit var vm : MainContracts.VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = vmProvider.provideVM(this)
}
override fun getVMClass(): KClass<MainContracts.VM> = MainContracts.VM::class
}
It's a long way to go, but after you have the initial setup is completed, all you need to do for individual fragments is to make them implement VMDependant and provide a concrete class for YourAbsViewModel in Hilt using the #ViewModelKey.
In tests, vmProvider can then be easily mocked and forced to do your bidding.
I'm trying to make a shared injected view model between a fragment and an activity using the Jetpack tutorial.
The shared view model is successfully injected into the parent MyActivity but when the child is rendered, the application crashes due to dependency injection failure. I have provided the code below that created the issue.
Providing the Session Manager:
#InstallIn(ApplicationComponent::class)
#Module
class AppModule {
#Provides
#Singleton
fun provideSessionManager(
networkClient: NetworkClient
): SessionManager {
return SessionManager(networkClient)
}
}
To be injected into the Shared View Model:
class SharedViewModel #ViewModelInject constructor(
private var sessionManager: SessionManager
) : ViewModel() {
var name = MutableLiveData<String>("Shared View Model")
}
And is used by both a parent activity and child fragment.
class MyActionFragment() : Fragment() {
private val viewModel: SharedViewModel by viewModels()
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("View Model Name 1: ${viewModel.name.value}") // This line crashes
}
}
class MyActivity : AuthenticatedBaseActivity() {
private val viewModel: SharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("View Model Name 2: ${viewModel.name.value}") // This line prints
}
}
However, when the code is run, notice the activity created the ViewModel and accessed its values, but when the fragment tried to do the same, the application crashes:
**D/MyActivity: View Model Name 2: Shared View Model**
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.xxx.xxx, PID: 16630
java.lang.RuntimeException: Cannot create an instance of class com.xxx.xxx.ui.main.SharedViewModel
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.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.xxx.xxx.ui.main.MyActionFragment.getViewModel(Unknown Source:2)
at com.xxx.xxx.ui.main.MyActionFragment.onActivityCreated(**MyActionFragment.kt:140**)
at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:2718)
Additionally, when I remove the Hilt dependency injected sessionManager the fragment and view model are created without an issue.
Followed this post with no luck.
Any help on Hilt view model dependency injection with a shared model would be extremely appreciated!! Thanks!
You can use extension function in Fragment:
class MyFragment: Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
}
And in Activity:
class MyActivity : Activity() {
private val viewModel: SharedViewModel by viewModels()
}
You must provide all dependency , In your case NetworkClient not provided
#Module
#InstallIn(ApplicationComponent::class)
object AppModule {
#Singleton
#Provides
fun provideSessionManager(
networkClient: NetworkClient
): SessionManager = SessionManager(networkClient)
#Singleton
#Provides
fun provideNetworkClient() = NetworkClient()
}
In the Activity or Fragment use #AndroidEntryPoint
#AndroidEntryPoint
class MyActionFragment() : Fragment()
#AndroidEntryPoint
class MyActivity : AuthenticatedBaseActivity()
To share data between activity and fragments. use the below code. Hilt doc didn't work for me also.
In Activity
private val vm by viewModels<StartVM>()
In Fragment
private val vm: StartVM by lazy {
obtainViewModel(requireActivity(), StartVM::class.java, defaultViewModelProviderFactory)
}
Kotlin extension
fun <T : ViewModel> Fragment.obtainViewModel(owner: ViewModelStoreOwner,
viewModelClass: Class<T>,
viewmodelFactory: ViewModelProvider.Factory
) =
ViewModelProvider(owner, viewmodelFactory).get(viewModelClass)
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
We're using Dagger2 in our application. I am trying to do a room database and I am writing the repository code, but I would like to inject application context and the DAO for the class.
I have a feeling that you can only do Dagger injection in Fragments, Activities, Services, Applications, etc.
Here's what I have:
class DownloadsDataRepositoryImpl : IDownloadsDataRepository, HasAndroidInjector {
#Inject
lateinit var androidInjector : DispatchingAndroidInjector<Any>
#Inject
lateinit var downloadsDao: DownloadsDao
override fun androidInjector(): AndroidInjector<Any> = androidInjector
init {
androidInjector()
}
}
But I'm sure it's not going to work. Is there a way to do it?
As stated, dagger-android is just a tool to help injecting specific framework classes that you can't have control on it's creation.
The proper approach is to use simple construction injection.
To be more direct on how you should expose it on your #Component, I would need more code, specifically on what you have on your activity/fragment, but here is a crude example (that I did not tested, if there are minor errors, you can fix them following the compiler error messages):
First, you will have some object that exposes your DAO. Probably it's room?
#Entity(tableName = "download_table")
data class DownloadEntity(
#PrimaryKey
val key: String
)
#Dao
interface DownloadsDao {
#Query("SELECT * FROM download_table")
fun load(): List<DownloadEntity>
}
#Database(
entities = [DownloadEntity::class], version = 1
)
abstract class DownloadRoomDatabase : RoomDatabase() {
abstract val downloadsDao: DownloadsDao
}
Now we will create a crude repository that is build with #Inject annotation. Dagger will take care of building this object for us. Notice that I am not using dagger-android for it:
interface IDownloadsDataRepository
class DownloadsDataRepositoryImpl #Inject constructor(
val downloadsDao: DownloadsDao
) : IDownloadsDataRepository
How to expose it to your activity/fragment/service requires more details on your implementation. For example, if it's inside a ViewModel or a Presenter that is annotated with #Inject or you are accessing directly on your activity will result in different implementations. Without more details, I will suppose that you are accessing the repository directly on your activity:
class DownloadActivity : FragmentActivity() {
#Inject
lateinit val repo: IDownloadsDataRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerDownloadComponent.factory().create(this).inject(this)
}
}
Now we need to instruct Dagger on how to:
Bind your concrete DownloadsDataRepositoryImpl to the IDownloadsDataRepository interface that the activity requires
How to provide the dependencies to build DownloadsDataRepositoryImpl
For this we will need a module:
#Module
abstract class RepositoryModule {
//We will bind our actual implementation to the IDownloadsDataRepository
#Binds
abstract fun bindRepo(repo: DownloadsDataRepositoryImpl): IDownloadsDataRepository
#Module
companion object {
//We need the database to get access to the DAO
#Provides
#JvmStatic
fun provideDataBase(context: Context): DownloadRoomDatabase =
Room.databaseBuilder(
context,
DownloadRoomDatabase::class.java,
"download_database.db"
).build()
//With the database, we can provide the DAO:
#Provides
#JvmStatic
fun provideDao(db: DownloadRoomDatabase): DownloadsDao = db.downloadsDao
}
}
With this, we can finish the last part of our puzzle, creating the #Component:
#Component(
modules = [
RepositoryModule::class
]
)
interface DownloadComponent {
fun inject(activity: DownloadActivity)
#Component.Factory
interface Factory {
fun create(context: Context): DownloadComponent
}
}
Notice that I did not use any dagger-android code, I don't think it's useful and causes more confusion than necessary. Stick with basic dagger2 constructs and you are fine. You can implement 99.9% of your app only understanding how those constructs works:
#Module, #Component and #Subcomponent
Edit: As stated in the comments, probably you will need to properly manage the scope of your repository, specially the DB creation if you are actually using Room.
Not sure how you implemented dagger, but here is an example how you can provide context to non activity class.
Suppose you have AppModule class, so there you can add provideContext() method:
#Module
class AppModule(app: App) {
private var application: Application = app
#Provides
fun provideContext(): Context {
return application
}
}
and here is non activity class written in Kotlin:
class Utils #inject constructor(private val context: Context) {
..
}
And that's it, just rebuild j
I have a feeling that you can only do Dagger injection in Fragments, Activities, Services, Applications, etc.
You were correct to assume that before Dagger-Android 2.20, but not after 2.20+.
Now you can create a #ContributesAndroidInjector for any class, which will generate an AndroidInjector<T> for that T for which you added #ContributesAndroidInjector.
This means that there is a multi-binding that allows you to get an AndroidInjector<T> for a T, and this is what HasAndroidInjector does for you.
So the following worked for me in a different scenario (for member-injecting Workers in work-manager, instead of creating a multi-binding and a factory):
#Keep
class SyncWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
init {
val injector = context.applicationContext as HasAndroidInjector
injector.androidInjector().inject(this)
}
#Inject
lateinit var apiService: ApiService
and
#ContributesAndroidInjector
abstract fun syncWorker(): SyncWorker
HOWEVER in your particular case, none of this is required.
Dagger-Android is for member-injecting classes using an auto-generated subcomponent, that you typically need only if your injected type is inside a different module, and therefore you can't directly add fun inject(T t) into your AppComponent, OR you don't see your AppComponent.
In your case, simple constructor injection is enough, as you own your own class.
#Singleton
class DownloadsDataRepositoryImpl #Inject constructor(
private val downloadsDao: DownloadsDao
): IDownloadsDataRepository {}
Which you can bind via a module
#Module
abstract class DownloadsModule {
#Binds
abstract fun dataRepository(impl: DownloadsDataRepositoryImpl): IDownloadsDataRepository
}
And otherwise you just create your component instance inside Application.onCreate()
#Component(modules = [DownloadsModule::class])
#Singleton
interface AppComponent {
fun dataRepository(): DownloadsDataRepository
#Component.Factory
interface Factory {
fun create(#BindsInstance appContext: Context): AppComponent
}
}
And
class CustomApplication: Application() {
lateinit var component: AppComponent
private set
override fun onCreate() {
super.onCreate()
component = DaggerAppComponent.factory().create(this)
}
}
Then you can get it as
val component = (context.applicationContext as CustomApplication).component
Though technically you may as well create an extension function
val Context.appComponent: AppComponent
get() = (applicationContext as CustomApplication).component
val component = context.appComponent
Dagger version is 2.25.2.
I have two Android project modules: core module & app module.
In core module, I defined for dagger CoreComponent ,
In app module I have AppComponent for dagger.
CoreComponet in core project module:
#Component(modules = [MyModule::class])
#CoreScope
interface CoreComponent {
fun getMyRepository(): MyRepository
}
In core project module, I have a repository class, it doesn't belong to any dagger module but I use #Inject annotation next to its constructor:
class MyRepository #Inject constructor() {
...
}
My app component:
#Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
#featureScope
interface AppComponent {
fun inject(activity: MainActivity)
}
In MainActivity:
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val coreComponent = DaggerCoreComponent.builder().build()
DaggerAppComponent
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)
}
}
My project is MVVM architecture, In general:
MainActivity hosts MyFragment
MyFragment has a reference to MyViewModel
MyViewModel has dependency MyRepository (as mentioned above MyRepository is in core module)
Here is MyViewModel :
class MyViewModel : ViewModel() {
// Runtime error: lateinit property repository has not been initialize
#Inject
lateinit var repository: MyRepository
val data = repository.getData()
}
MyViewModel is initialized in MyFragment:
class MyFragment : Fragment() {
lateinit var viewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
...
}
}
When I run my app, it crashes with runtime error:
kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialize
The error tells me dagger dependency injection does't work with my setup. So, what do I miss? How to get rid of this error?
==== update =====
I tried :
class MyViewModel #Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
Now when I run the app, I get new error:
Caused by: java.lang.InstantiationException: class foo.bar.MyViewModel has no zero argument constructor
====== update 2 =====
Now, I created MyViewModelFactory:
class MyViewModelFactory #Inject constructor(private val creators: Map<Class<out ViewModel>,
#JvmSuppressWildcards Provider<ViewModel>>): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
try {
#Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
I updated MyFragment to be :
class MyFragment : Fragment() {
lateinit var viewModel: MyViewModel
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onAttach(context: Context) {
// inject app component in MyFragment
super.onAttach(context)
(context.applicationContext as MyApplication).appComponent.inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// I pass `viewModelFactory` instance here, new error here at runtime, complaining viewModelFactory has not been initialized
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
...
}
}
Now I run my app, I get new error:
kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
What's still missing?
In order to inject dependencies Dagger must be either:
responsible for creating the object, or
ask to perform an injection, just like in the activities or fragments, which are instantiated by the system:
DaggerAppComponent
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)
In your first approach none of the above is true, a new MyViewModel instance is created outside Dagger's control:
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
therefore the dependency doesn't even get initialized. Additionally, even if you'd perform the injection more manually, like in the activity, the code still would fail, because you are trying to reference the repository property during the initialization process of the object val data = repository.getData(), before the lateinit var gets a chance to be set. In such cases the lazy delegate comes handy:
class MyViewModel : ViewModel() {
#Inject
lateinit var repository: MyRepository
val data by lazy { repository.getData() }
...
}
However, the field injection isn't the most desirable way to perform a DI, especially when the injectable objects needs to know about it. You can inject your dependencies into ViewModels using the construction injection, but it requires some additional setup.
The problem lies in the way view models are created and managed by the Android SDK. They are created using a ViewModelProvider.Factory and the default one requires the view model to have non-argument constructor. So what you need to do to perform the constructor injection is mainly to provide your custom ViewModelProvider.Factory:
// injects the view model's `Provider` which is provided by Dagger, so the dependencies in the view model can be set
class MyViewModelFactory<VM : ViewModel> #Inject constructor(
private val viewModelProvider: #JvmSuppressWildcards Provider<VM>
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
viewModelProvider.get() as T
}
(There are 2 approaches to implementing a custom ViewModelProvider.Factory, the first one uses a singleton factory which gets a map of all the view models' Providers, the latter (the one above) creates a single factory for each view model. I prefer the second one as it doesn't require additional boilerplate and binding every view model in Dagger's modules.)
Use the constructor injection in your view model:
class MyViewModel #Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
And then inject the factory into your activities or fragments and use it to create the view model:
#Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
#featureScope
interface AppComponent {
fun inject(activity: MainActivity)
fun inject(fragment: MyFragment)
}
class MyFragment : Fragment() {
#Inject
lateinit var viewModelFactory: MyViewModelFactory<MyViewModel>
lateinit var viewModel: MyViewModel
override fun onAttach(context: Context) {
// you should create a `DaggerAppComponent` instance once, e.g. in a custom `Application` class and use it throughout all activities and fragments
(context.applicationContext as MyApp).appComponent.inject(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)[MyViewModel::class.java]
...
}
}
A few steps you'll need to use Dagger with the AAC ViewModel classes:
You need to use constructor injection in your ViewModel class (as you're doing in the updated question)
You will need a ViewModelFactory to tell the ViewModelProvider how to instantiate your ViewModel
Finally, you will need to tell Dagger how to create your ViewModelFactory
For the first step, pass the repository in the ViewModel constructor and annotate your view model class with #Inject:
class MyViewModel #Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
For the second and third steps, one easy way to create a generic ViewModelFactory for any ViewModels that you will have in your project, and also tell Dagger how to use it you can:
Create a Singleton generic ViewModelFactory:
#Singleton
class ViewModelFactory #Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
viewModels[modelClass]?.get() as T
}
Create a custom annotation to identify your ViewModels and let Dagger know that it needs to provide them:
#Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
Create a new module for your ViewModels:
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindsViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
// Add any other ViewModel that you may have
#Binds
#IntoMap
#ViewModelKey(MyViewModel::class)
internal abstract fun bindsMyViewModel(viewModel: MyViewModel): ViewModel
}
Don't forget to declare the new module in your dagger component
And use the view model in your activity, instantiating it with the help of the ViewModelFactory:
class MyFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var viewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
...
}
}