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:
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 ViewModel
s 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' Provider
s, 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:
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)
...
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With