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架构,总的来说:

这里是 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:

  1. 您需要在 ViewModel 中使用构造函数注入 class(正如您在更新后的问题中所做的那样)
  2. 您将需要一个 ViewModelFactory 来告诉 ViewModelProvider 如何实例化您的 ViewModel
  3. 最后,您需要告诉 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 的最理想方式,尤其是当可注入对象需要了解它时。您可以使用构造注入将依赖项注入 ViewModels,但它需要一些额外的设置。

问题在于 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]
        ...
    }
}