Dagger 2:多模块项目,注入依赖但在运行时出现 "lateinit property repository has not been initialize" 错误
Dagger 2: multi-module project, inject dependency but get "lateinit property repository has not been initialize" error at runtime
Dagger 版本为 2.25.2。
我有两个 Android 项目模块:core
模块和 app
模块。
在core
模块中,我为dagger定义了CoreComponent
,
在 app
模块中我有 AppComponent
匕首。
CoreComponet
在核心项目模块中:
@Component(modules = [MyModule::class])
@CoreScope
interface CoreComponent {
fun getMyRepository(): MyRepository
}
在核心项目模块中,我有一个存储库 class,它不属于任何 dagger 模块,但我在其构造函数旁边使用了 @Inject
注释:
class MyRepository @Inject constructor() {
...
}
我的应用组件:
@Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
@featureScope
interface AppComponent {
fun inject(activity: MainActivity)
}
在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)
}
}
我的项目是MVVM架构,总的来说:
MainActivity
主机 MyFragment
MyFragment
引用了 MyViewModel
MyViewModel
有依赖MyRepository
(如上所说MyRepository
在core
模块中)
这里是 MyViewModel
:
class MyViewModel : ViewModel() {
// Runtime error: lateinit property repository has not been initialize
@Inject
lateinit var repository: MyRepository
val data = repository.getData()
}
MyViewModel
在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)
...
}
}
当我 运行 我的应用程序时,它因 运行 时间错误而崩溃:
kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialize
错误告诉我 Dagger 依赖注入不适用于我的设置。那么,我想念什么?如何摆脱这个错误?
====更新=====
我试过了:
class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
现在当我 运行 应用程序时,我收到新错误:
Caused by: java.lang.InstantiationException: class foo.bar.MyViewModel has no zero argument constructor
======更新2 =====
现在,我创建了 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)
}
}
}
我将 MyFragment 更新为:
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)
...
}
}
现在我 运行 我的应用程序,我收到新错误:
kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
还缺少什么?
将 Dagger 与 AAC ViewModel 结合使用需要执行几个步骤 classes:
- 您需要在 ViewModel 中使用构造函数注入 class(正如您在更新后的问题中所做的那样)
- 您将需要一个 ViewModelFactory 来告诉 ViewModelProvider 如何实例化您的 ViewModel
- 最后,您需要告诉 Dagger 如何创建您的 ViewModelFactory
第一步,在 ViewModel 构造函数中传递存储库,并使用 @Inject
:
注释您的视图模型 class
class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
对于第二步和第三步,一种简单的方法可以为项目中的任何 ViewModel 创建通用 ViewModelFactory,并告诉 Dagger 如何使用它,您可以:
创建单例通用 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
}
创建自定义注释来标识您的 ViewModel 并让 Dagger 知道它需要提供它们:
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
为您的 ViewModel 创建一个新模块:
@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
并在 activity 中使用视图模型,在 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)
...
}
}
为了注入依赖项,Dagger 必须是:
- 负责创建对象,或者
- 要求执行注入,就像在系统实例化的活动或片段中一样:
DaggerAppComponent
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)
在您的第一种方法中,上述 none 是正确的,在 Dagger 的控制之外创建了一个新的 MyViewModel
实例:
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
因此依赖项甚至没有被初始化。此外,即使您更手动地执行注入,例如在 activity 中,代码仍然会失败,因为您试图在初始化过程中引用 repository
属性对象 val data = repository.getData()
,在 lateinit var
有机会被设置之前。在这种情况下,lazy
委托就派上用场了:
class MyViewModel : ViewModel() {
@Inject
lateinit var repository: MyRepository
val data by lazy { repository.getData() }
...
}
但是,字段注入并不是执行 DI 的最理想方式,尤其是当可注入对象需要了解它时。您可以使用构造注入将依赖项注入 ViewModel
s,但它需要一些额外的设置。
问题在于 Android SDK 创建和管理视图模型的方式。它们是使用 ViewModelProvider.Factory
创建的,默认情况下要求视图模型具有无参数构造函数。所以执行构造函数注入需要做的主要是提供你自定义的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
}
(有两种实现自定义 ViewModelProvider.Factory
的方法,第一种使用单例工厂获取所有视图模型的映射 Provider
,后者(上面的那个) 为每个视图模型创建一个工厂。我更喜欢第二个,因为它不需要额外的样板和绑定 Dagger 模块中的每个视图模型。)
在视图模型中使用构造函数注入:
class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
然后将工厂注入到您的活动或片段中,并使用它来创建视图模型:
@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]
...
}
}
Dagger 版本为 2.25.2。
我有两个 Android 项目模块:core
模块和 app
模块。
在core
模块中,我为dagger定义了CoreComponent
,
在 app
模块中我有 AppComponent
匕首。
CoreComponet
在核心项目模块中:
@Component(modules = [MyModule::class])
@CoreScope
interface CoreComponent {
fun getMyRepository(): MyRepository
}
在核心项目模块中,我有一个存储库 class,它不属于任何 dagger 模块,但我在其构造函数旁边使用了 @Inject
注释:
class MyRepository @Inject constructor() {
...
}
我的应用组件:
@Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
@featureScope
interface AppComponent {
fun inject(activity: MainActivity)
}
在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)
}
}
我的项目是MVVM架构,总的来说:
MainActivity
主机MyFragment
MyFragment
引用了MyViewModel
MyViewModel
有依赖MyRepository
(如上所说MyRepository
在core
模块中)
这里是 MyViewModel
:
class MyViewModel : ViewModel() {
// Runtime error: lateinit property repository has not been initialize
@Inject
lateinit var repository: MyRepository
val data = repository.getData()
}
MyViewModel
在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)
...
}
}
当我 运行 我的应用程序时,它因 运行 时间错误而崩溃:
kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialize
错误告诉我 Dagger 依赖注入不适用于我的设置。那么,我想念什么?如何摆脱这个错误?
====更新=====
我试过了:
class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
现在当我 运行 应用程序时,我收到新错误:
Caused by: java.lang.InstantiationException: class foo.bar.MyViewModel has no zero argument constructor
======更新2 =====
现在,我创建了 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)
}
}
}
我将 MyFragment 更新为:
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)
...
}
}
现在我 运行 我的应用程序,我收到新错误:
kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
还缺少什么?
将 Dagger 与 AAC ViewModel 结合使用需要执行几个步骤 classes:
- 您需要在 ViewModel 中使用构造函数注入 class(正如您在更新后的问题中所做的那样)
- 您将需要一个 ViewModelFactory 来告诉 ViewModelProvider 如何实例化您的 ViewModel
- 最后,您需要告诉 Dagger 如何创建您的 ViewModelFactory
第一步,在 ViewModel 构造函数中传递存储库,并使用 @Inject
:
class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
对于第二步和第三步,一种简单的方法可以为项目中的任何 ViewModel 创建通用 ViewModelFactory,并告诉 Dagger 如何使用它,您可以:
创建单例通用 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
}
创建自定义注释来标识您的 ViewModel 并让 Dagger 知道它需要提供它们:
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
为您的 ViewModel 创建一个新模块:
@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
并在 activity 中使用视图模型,在 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)
...
}
}
为了注入依赖项,Dagger 必须是:
- 负责创建对象,或者
- 要求执行注入,就像在系统实例化的活动或片段中一样:
DaggerAppComponent
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)
在您的第一种方法中,上述 none 是正确的,在 Dagger 的控制之外创建了一个新的 MyViewModel
实例:
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
因此依赖项甚至没有被初始化。此外,即使您更手动地执行注入,例如在 activity 中,代码仍然会失败,因为您试图在初始化过程中引用 repository
属性对象 val data = repository.getData()
,在 lateinit var
有机会被设置之前。在这种情况下,lazy
委托就派上用场了:
class MyViewModel : ViewModel() {
@Inject
lateinit var repository: MyRepository
val data by lazy { repository.getData() }
...
}
但是,字段注入并不是执行 DI 的最理想方式,尤其是当可注入对象需要了解它时。您可以使用构造注入将依赖项注入 ViewModel
s,但它需要一些额外的设置。
问题在于 Android SDK 创建和管理视图模型的方式。它们是使用 ViewModelProvider.Factory
创建的,默认情况下要求视图模型具有无参数构造函数。所以执行构造函数注入需要做的主要是提供你自定义的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
}
(有两种实现自定义 ViewModelProvider.Factory
的方法,第一种使用单例工厂获取所有视图模型的映射 Provider
,后者(上面的那个) 为每个视图模型创建一个工厂。我更喜欢第二个,因为它不需要额外的样板和绑定 Dagger 模块中的每个视图模型。)
在视图模型中使用构造函数注入:
class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
然后将工厂注入到您的活动或片段中,并使用它来创建视图模型:
@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]
...
}
}