阅读 309

【译】在 Kotlin 开发的 Android 应用中如何使用 Dagger

原文地址 —— 官方 Dagger 手把手入门教程

1. 简介

在本文中,你将学到依赖注入(DI)对于创建一个稳健且可扩展的大规模应用的重要性。我们将使用 Dagger 作为 DI 工具来管理依赖。

依赖项注入 (DI) 是一种广泛用于编程的技术,非常适用于 Android 开发。遵循 DI 的原则可以为良好的应用架构奠定基础。

实现依赖项注入可为您带来以下优势:

  • 重用代码
  • 易于重构
  • 易于测试

预备知识

  • 有使用过 Kotlin
  • 理解依赖注入并且了解在 Android 应用中使用 Dagger 有哪些好处

可以在👇找到更多有关依赖注入和如何使用 Dagger 的知识点

  1. Android 中的依赖项注入
  2. 手动依赖项注入
  3. Android 中使用 Dagger

你将学到什么

  • 如何在大规模的 Android 应用中使用 Dagger
  • 明确 Dagger 的思想来创建一个更加稳健和可持续维护的应用
  • 为什么需要 Dagger 的子组件以及如何使用它们
  • 如何使用 Dagger 来完成应用的单元和插桩测试

在这篇文章结束后,你将完成创建并测试得到像下面这样的一个应用类的依赖图表:

2. 准备工作

获取代码

这里 或者 github 这里 获取源码,并按照 README 的说明完成工程配置。

译者注:建议从 master 切出分支,开始边阅读边编码

工程中添加 Dagger 库

打开工程的配置文件 app/build.gradle ,然后添加两个 Dagger 依赖,然后在文件的顶部加入 kapt plugin,如下:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

...

dependencies {
    ...
    def dagger_version = "2.27"
    implementation "com.google.dagger:dagger:$dagger_version"
    kapt "com.google.dagger:dagger-compiler:$dagger_version"
}
复制代码

添加上述配置后,点击 “Sync Now” 工程自动下载相关配置后,我们就可以开始使用 Dagger 了。

kapt

Dagger 是始于 Java 的注解模型来实现的,该模型会在编译时使用 “注解处理器” 来生成代码。而在 Kotlin 中,将使用 kapt 编译插件来实现 “注解处理器”,即对应上文提到的配置语句 apply plugin: 'kotlin-kapt'apply plugin: 'kotlin-android-extensions'

译者注:这和 Java 的配置语句是不同的,Java 是直接使用 javac 生成语句,使用 annotationProcessor 即可。

只是运行时生成语句

在上面配置的依赖中,dagger 库包含了你所能使用的所有注解,dagger-compiler 作为注解处理器,为我们生成代码。它本身并不会在打包 App 时塞到应用包里面。

这里 我们可以获取最新的 Dagger 发布信息。

3、从 @Inject 开始 —— 构造器注入和属性注入

让我们开始使用 Dagger 重构注册流程。

Dagger 为了可以帮我们自动构建应用类的依赖图,它需要我们要告诉它,在应用中,如何创建类的实例。方法之一就是通过在类的构造函数加上 @Inject。这个类的成员属性将视为一个依赖类型。

打开 RegistrationViewModel.kt 文件并使用下面的语句替换这个类的定义申明:

// @Inject tells Dagger how to provide instances of this type
// Dagger also knows that UserManager is a dependency
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}
复制代码

如上所示,在 Kotlin 语法中,在某个类的构造函数上运用一个注解,你需要明确地加上关键字 constructor ,然后在它的前面加上该注解即可。

使用 @Inject 注解后,Dagger 就可以知道:

  • 如何创建 RegistrationViewModel 类的实例。
  • RegistrationViewModel 有一个 UserManager 作为依赖,当构造它时需要一个 UserManager 作为参数。

题外话:

现在才刚刚开始,为了简化问题,RegistrationViewModel 它并不是我们熟知的 Android 框架组件中的 ViewModel ;它仅仅是一个常规类,扮演并承担 ViewModel 的职责。

如果想获取更多关于在 Android 框架组件中使用 Dagger 的有关信息,可以到官方的 Android 蓝图代码库 自行下载并学习。

但是,Dagger 并不知道如何创建 UserManager ,遵循上面同样的处理,我们就可以在 UserManager 的构造函数前添加 @Inject

打开 UserManager.kt 文件并使用下面的语句替换这个类的定义申明:

class UserManager @Inject constructor(private val storage: Storage) {
    ...
}
复制代码

现在,Dagger 就知道如何提供 RegistrationViewModelUserManager 的实例了。

然而 UserManager 的依赖是一个接口,我们需要使用不同的方式来告诉 Dagger 如何创建它的实例,我们将稍后在讲解,目前先按下不表。

明显的 Android 框架自身的类,如 Activities 以及 Fragments 它们都是被系统初始化的,所以 Dagger 是不能为开发者创建它们的。Activities 大多数情况下,对于开发者,所有的初始化代码需要在 onCreate 方法执行。正因为如此,我们不能在一个视图类的构造器之前使用 @Inject 注解(也就是通常说的构造器注入)。取而代之,我们不得不使用属性注入。

在我们的应用中,RegistrationActivity 有一个关于 RegistrationViewModel 的依赖。如果你打开 RegistrationActivity ,我们可以看到在 onCreate 方法中在调用 supportFragmentManager 之前我们有创建这个 ViewModel。我们并不想手动创建它,我们想要 Dagger 来提供它。就此,我们需要:

  • 为这个属性加上 @Inject 注解
  • 从 onCreate() 方法中,移除它的初始化
class RegistrationActivity : AppCompatActivity() {

    // @Inject annotated fields will be provided by Dagger
    @Inject
    lateinit var registrationViewModel: RegistrationViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Remove following line
        registrationViewModel = RegistrationViewModel((application as MyApplication).userManager)
    }
}
复制代码

构造器注入,它告诉 Dagger 如何提供一个类的实例;属性注入,它告诉 Dagger 这个类需要填充哪种类型的成员变量。

我们是怎样能告知 Dagger 哪些对象需要被注入到 RegistrationActivity 呢?我们需要创建一个 Dagger 依赖图(或者叫应用依赖图),然后通过它来注入对象到 Activity 中。

4、@Component 注解 —— 实现依赖图的容器

我们想要 Dagger 创建我们工程的依赖图,为我们管理它们,并能够从这个图中获取所有的依赖对象。为了 Dagger 可以实现上述要求,我们需要创建一个接口并给它加上注解 @Component 。Dagger 将创建一个容器,为开发者实现手动依赖注入。

An interface annotated with @Component will make Dagger generate code with all the dependencies required to satisfy the parameters of the methods it exposes. Inside that interface, we can tell Dagger that RegistrationActivity requests injection.

下面我们在示例工程的 com.example.android.dagger 包下,创建一个新的包 di 。在这个包里,创建一个新的 Kotlin 文件 AppComponent.kt ,然后在文件中定义如下一个接口:

package com.example.android.dagger.di

import com.example.android.dagger.registration.RegistrationActivity
import dagger.Component

// Definition of a Dagger component
@Component
interface AppComponent {
    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
}
复制代码

如上所示,在 @Component 接口中定义 inject(activity: RegistrationActivity) 方法,就等同于开发者告知 Dagger RegistrationActivity 需要注入,同时它必须提供那些用 @Inject 标记的依赖对象( 比如:上一节中我提到过的 RegistrationViewModel )。

由于 Dagger 必须创建一个 RegistrationViewModel 实体对象,意味着,它也要创建令 RegistrationViewModel 满意的依赖对象( 比如:UserManager )。以此类推,Dagger 递归处理发现所有依赖对象,如果当它发现某一个依赖对象不知道如何提供时,它将在编译时向开发者报错,告知它不能让对应的这个依赖对象满意。

小结:一个 @Component 接口为 Dagger 提供了在编译时需要生成的依赖图的信息。需要注入的类作为该接口方法的参数类型。

添加上述代码后,下面可以尝试编译整个项目,不难发现如下错误:

dagger/app/build/tmp/kapt3/stubs/debug/com/example/android/dagger/di/AppComponent.java:7: error: [Dagger/MissingBinding] com.example.android.dagger.storage.Storage cannot be provided without an @Provides-annotated method

让我们分析一下这个错误信息。首先,它告诉我们这个错误发生于 AppComponent 。这个错误的类型是 [Dagger/MissingBinding] 意味着 Dagger 不知道如何提供一个明确的类型。最后,它告知 Storage 不能被提供。

也就是,我们没有告诉 Dagger 怎样提供一个 UserManager 所需要的 Storage 对象 !

5、@Module、@Binds、@BindsInstance 注解

由于 Storage 是一个接口,它是不能直接初始化的,因此,我们告知 Dagger 如何提供一个 Storage 实体对象的方式是不一样的。我们需要告诉 Dagger ,我们想要使用 Storage 的哪个实现类。在这个例子中实现类为 SharedPreferencesStorage

为了实现上述情况,我们将用到一个 Dagger Module 。Dagger Module 是一个用 @Module 注解的类。

类似 Components,Dagger Module 也是一个用于和 Dagger 沟通的类,它告诉 Dagger 如何提供一个明确的类型的实体。依赖对象使用 @Providers 和 @Binds 注解来定义。

更多关于 @Providers 的信息可以阅读官方文档 Using Dagger in Android apps documentation 或者本文最后的章节。

由于这个 Module 将要包含有关 Storage 的信息,所以让我们在和我们之前创建 AppComponent.kt 的包下再新建一个命名为 StorageModule.kt 的文件。具体代码如下:

package com.example.android.dagger.di

import dagger.Module

// Tells Dagger this is a Dagger module
@Module
class StorageModule {

}
复制代码

@Bind 注解

使用 @Bind 可以告知 Dagger 当它需要提供一个接口时,可以使用该接口的哪个实现类。

@Bind 必须注解一个抽象方法。该抽象方法的返回值就是 Dagger 要提供的接口类型。接口对应的实现类作为该方法的参数。如下所示:

// Tells Dagger this is a Dagger module
// Because of @Binds, StorageModule needs to be an abstract class
@Module
abstract class StorageModule {

    // Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
    @Binds
    abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}
复制代码

上面的代码,我们告诉 Dagger “当你需要一个 Storage 的对象,请使用 SharedPreferencesStorage”

要点:

  • provideStorage 仅仅是一个抽象方法的名字,它可以命名为任何我们喜欢的名字,Dagger 并不关心它叫什么。Dagger 只会关心它的返回值和参数。

  • 这里的 StorageModule 是抽象类,只不过是因为 provideStorage 是抽象方法。

Modules 是一个语义层的路径,它封装了告知 Dagger 如何创建某些对象的语义。正如上所示,我们命名了 StorageModule 类,并在其中加入了有关如何提供一个 Storage 对象的逻辑。假如后续扩展我们的应用,我们也可以提供不同于 SharedPreferencesStorage 的实现类。

好了,上面我们告知了 Dagger 当要求生成一个 Storage 对象时,应该使用一个 SharedPreferencesStorage 的实例,但是我们现在还没有告诉 Dagger 如何创建一个 SharedPreferencesStorage 。正如之前一样,我们可以通过在 SharedPreferencesStorage 的构造器前加上 @Inject 来达到这一目的。

// @Inject tells Dagger how to provide instances of this type
class SharedPreferencesStorage @Inject constructor(context: Context) : Storage { ... }
复制代码

此外,我们的应用依赖图需要知道有 StorageModule 的存在。因此,需要在 AppComponent 中加入它,即在注解 @Component 作为 modules 参数的值,如下所示:

// Definition of a Dagger component that adds info from the StorageModule to the graph
@Component(modules = [StorageModule::class])
interface AppComponent {
    
    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
}
复制代码

这样,AppComponent 就可以访问所有有关 StorageModule 包含的信息。在更复杂的应用中,我们可能也许还有一个 NetworkModule ,比如:用来它告知容器如何提供一个 OkHttpClient、如何配置一个 Gson 或者 Moshi,等等。

好,如果现在再一次 Build 我们的工程,你会发现我们之前出现过的报错信息!Dagger 没法找到 Context

@BindsInstance annotation

我们如何告知 Dagger 提供一个 Context 呢? Context 是 Android 系统提供的一个对象,因此它的创建一定是发生于应用依赖图之外的。由于在有依赖图之前 Context 就已经有效的存在,我们可以在依赖图中创建一个实体,然后将 Context 赋值给它。

赋值的途径就是通过定义一个 Component Factory 并结合 @BindsInstance 注解,如下:

@Component(modules = [StorageModule::class])
interface AppComponent {

    // Factory to create instances of the AppComponent
    @Component.Factory
    interface Factory {
        // With @BindsInstance, the Context passed in will be available in the graph
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun inject(activity: RegistrationActivity)
}
复制代码

如上所示,我们使用 @Component.Factory 注解,声明了一个接口,此外,它还包含了一个方法,方法的返回类型是一个 Component ,同时有个使用 @BindsInstance 注解的类型为 Context 的参数。

@BindsInstance 告诉 Dagger ,Dagger 需要添加一个实例到依赖图容器里面,同时无论什么时候需要一个 Context 对象时,都可以使用这个实例。

使用 @BindsInstance —— 当某个对象创建于依赖图容器之外(如:示例中的 Context)

🤩好,现在我们初步完成了使用 Dagger 重构注册流程

再次 Build 工程,现在不会再报错了... Dagger 已经成功生成了应用依赖图,并且你可以开始使用它们了。

这个应用依赖图是通过注解处理器自动生成的。生成的类被命名为 Dagger{ComponentName} ,它包含了依赖图的实现。我们将在下一章节使用生成的类 DaggerAppComponent

现在的 AppComponent 依赖图看起来是什么样子呢?

AppComponent 包含了一个 StorageModule ,该 Module 告知 Dagger 如何创建一个 Storage 实例。Storage 有一个 Context 的依赖,但是由于我们在创建依赖图时,我们提供了这个 Context ,因此 Storage 也就隐含的依赖了这个 Context

这个 Context 的实例是通过 AppComponent 的工厂的 create 方法来创建。因此,Context 可以视为一个 “全局” 对象,在上面的图中,我们通过在它的左上角加一个白点来标识。

现在,RegistrationActivity 可以访问 Dagger 的依赖图容器来获取注入的对象了,比如在这个例子中的 RegistrationViewModel (因为这个成员变量前加了注解 @Inject)。

下面描述一下,编译器生成依赖图容器后,Dagger 是如何完成自动注入的:

作为 AppComponent ,它需要为 RegistrationActivity 注入 RegistrationViewModel ,它需要创建一个 RegistrationViewModel 的实例。为了实现这些,它需要提供 RegistrationViewModel 所有的依赖,进而它也需要创建一个 UserManager 的实例。

作为 UserManager ,它的构造器使用了 @Inject 注解,Dagger 将使用这个构造器来创建实例。 UserManager 又有一个 Storage 的依赖,但是它已经在依赖图中,不需要做其他事情了。

6、在 Activity 中使用 Dagger 注入

在 Android 中,开发者通常需要在自定义的 Application 中创建一个 Dagger 依赖图容器,因为开发者需要当 App 运行时,这个依赖图容器一直存在于内存之中。这样一来,依赖图便依附于 App 的生命周期内。在我们的例子中,我们也想在依赖图中有一个 application Context 。这样的好处是,这个依赖图可以被其他的 Android 框架类使用(通过它们的 Context 可以访问到),同时这样也有利于测试,由于你使用了一个自定义的 Application 类。

下面让我们在自定义的 Application —— MyApplication 中,添加一个依赖图实例:

open class MyApplication : Application() {

    // Instance of the AppComponent that will be used by all the Activities in the project
    val appComponent: AppComponent by lazy {
        // Creates an instance of AppComponent using its Factory constructor
        // We pass the applicationContext that will be used as Context in the graph
        DaggerAppComponent.factory().create(applicationContext)
    }

    open val userManager by lazy {
        UserManager(SharedPreferencesStorage(this))
    }
}
复制代码

正如我们在上一章节提到的,当我们构建整个工程时,Dagger 生成了一个名为 DaggerAppComponent 的类,它包含了AppComponent 依赖图的所有实现。由于我们使用了注解 @Component.Factory ,所以我们可以调用 .factory() ,它在 DaggerAppComponent 中是一个静态方法。于是,我们现在也可以调用,我们之前在这个 factory 接口中定义的 create 方法,在这里我们传入 applicationContext 作为 Context 参数的值。

这里我们使用了 Kotlin lazy 初始化 ,因此这个变量是不可变的,而且只有当被使用的时候才会初始化。

注意:如果你被 AndroidStudio 告知 DaggerAppComponent 不存在,可能就需要你 Build 一下工程,让 Dagger 的注解处理器去生成代码。因此,当有新的 Dagger 代码需要生效时,你必须再次 Build 工程。

接下来,我们就可以在 RegistrationActivity 中使用这个依赖图容器的实例,来让 Dagger 注入那些使用 @Inject 注解的成员变量的实例了。具体怎么做呢?我们可以调用 AppComponentinject 方法,把 RegistrationActivity 作为参数传入这个方法。

开发者不需要再自己去初始化这些属性,现在 Dagger 现在会帮开发者去初始化这些属性,代码如下:

class RegistrationActivity : AppCompatActivity() {

    // @Inject annotated fields will be provided by Dagger
    @Inject lateinit var registrationViewModel: RegistrationViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

        // Ask Dagger to inject our dependencies
        (application as MyApplication).appComponent.inject(this)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_registration)

        // REMOVE THIS LINE
        registrationViewModel = RegistrationViewModel((application as MyApplication).userManager)

        supportFragmentManager.beginTransaction()
            .add(R.id.fragment_holder, EnterDetailsFragment())
            .commit()
    }

    ...
}
复制代码

如上调用 appComponent.inject(this) 方法,以实现注入 RegistrationActivity 中使用 @Inject 注解的成员变量。

划重点: 当在 Activity 中使用 Dagger 注入时,inject 方法必须在 super.onCreate 方法之前调用,以避免恢复 fragment 时出现问题。因为在 super.onCreate 中,当对应的 Activity 处于恢复阶段,依附于它的 fragment 可能会访问该 Activity 的绑定。

RegistrationActivity 现在已经使用 Dagger 来管理它的依赖了。可以开始下一步,把整个工程运行起来试试吧。

发现 Bug 了没?在注册流程结束后,主页的登录页应该消失,但是并没有。为什么呢?因为其他流程并没有使用 Dagger 依赖图容器。

MainActivity 仍然在使用定义于 MyApplication 中的 userManager ,然而注册模块确实已经使用了一个来自于 Dagger 依赖图容器中的 userManager

现在让我们在主流程中也是用 Dagger ,以此来修复这个问题。

Using Dagger in the Main Flow

正如之前,我们想在 MainActivity 使用 Dagger 管理它的依赖。在这里,有 MainViewModelUserManager

译者注:这里不再赘述了,原文一大段描述,大概意思就是又按之前的走一遍,比如:要注入的属性加 @Inject ;在 AppComponent 这个容器接口中加入新的方法,该方法以 MainActivity 作为参数,等等。做完之后,我们可以得到一个新的依赖图,如下:

但是,还有一点需要注意:

划重点: Dagger 的属性注入,对应的属性不可以是私有的,该属性的可见性必须是包可见及其以上。

这时我们再次运行 App ,会发现仍然有 Bug ,因为 Dagger 默认每次都提供了一个新的注入属性,在这个例子中的 UserManager。那么我们如何让 Dagger 每次都重用相同的实体属性呢?这种情况下,就要用到 Scope (作用域)。

7、使用 Dagger 容器的作用域(Using Scopes)

某些时候,由于某些复杂的原因,开发者可能想 Dagger 在一个 Component 中提供一个全局的唯一的依赖对象实例,比如:

  • 开发者需要在多个依赖者中共享一个相同的实例(比如:本文例子中的 UserManager)。
  • 一个对象的初始化非常昂贵,由此开发者不想每次用的都重新创建一个新的对象(比如:Json 解析器)。

对 Dagger 容器使用作用域类型的注解(即:@Scope),可以实现得 Dagger 的容器内有一个唯一的实例。也可以称为 “ 和主容器生命周期一致的作用域类型 ”。和主容器声明周期一样的作用域类型,意味着任何使用 Dagger 容器注入的业务模块,如果需要 Dagger 提供一个对象,此时这个对象的类上加入了和主容器相同的注解,那么 Dagger 将会为所有的业务模块提供同一个该类型的对象。

译者注:

首先,上面这段文字算是理解 Dagger Scope 非常非常非常重要的一段;

然后,吐槽一下,上面这段并没有逐句按原文来翻译,而是结合本人的理解来翻译的,逐句翻译那注定是个悲剧,绝对没法懂,我太难了,由于它们想要解释的问题相对复杂,一堆从句绕的不行;

再然后,我想说的是下面提到的 @Singleton ,和例子中实现一个 “单例的” UserManager 对象没什么太大的关系,它就只是个名字,可能算得上见名识意,但是 Singleton 也就是个名字,之所以它能够作为一个作用域类型的注解,关键那是因为它用了 @Scope 。你完全可以换个名字,那你自定义一个 @Scope 的注解就行了。而且能够实现 “单例” 这个意思,那也是因为这个例子中的 AppComponent 它的实例定义在应用工程的自定义 Application 中,从整个应用来看, AppComponent 的实例就是单例的,所以说 @Singleton 就只是个名字而已。

再然后,就是 @Singleton 既然是个作用域名字,那这个主容器用了,其他作用域不同的容器就不能用了,因为一旦不同作用域的容器重名那你的让 Dagger 怎么定位它呢 ?同一个文件夹就不可能定义两个一模一样的文件名

最后,上面这一大段,对理解后面会用到的子容器作用域也会有帮助的。

为了达到上述目的,我们就可以用到 @Singleton 来实现,代码如下:

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent { ... }
复制代码

UserManager.kt

@Singleton
class UserManager @Inject constructor(private val storage: Storage) {
    ...
}
复制代码

至此,我们就解决了注册的问题

上面的代码可以在 1_registration_main 分支找到

下面是当下的依赖图:

Current state of the graph with a unique instance of UserManager in AppComponent

可以看到 UserManager 上加入了一个白点,类似 Context

8、Subcomponents

译者注:这里的一大段,我也不打算翻译了,太啰嗦了,简单来说,原文就是手把手继续使用 Dagger 来重构注册流程,EnterDetailsFragmentTermsAndConditionsFragment 仍然存在依赖问题,需要通过 Dagger 注入 ViewModel。

划重点:

An Activity injects Dagger in the onCreate method before calling super.

A Fragment injects Dagger in the onAttach method after calling super.

如果继续像上面那样,继续在 AppComponent 中加入,如下代码:

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {
    ...
    fun inject(activity: RegistrationActivity)
    fun inject(fragment: EnterDetailsFragment)
    fun inject(fragment: TermsAndConditionsFragment)
    fun inject(activity: MainActivity)
}
复制代码

照搬之前的注入方式就会报错了,因为 TermsAndConditionsFragment 和 EnterDetailsFragment,是要共用一个 RegistrationViewModel 的,这就需要使用 Dagger subcomponents 来解决。

We want the registration Fragments to reuse the same ViewModel coming from the Activity, but if the Activity changes, we want a different instance. We need to scope RegistrationViewModel to RegistrationActivity, for that, we can create a new Component for the registration flow and scope the ViewModel to that new registration Component. To achieve this we use Dagger subcomponents.

Dagger Subcomponents

由于 RegistrationViewModel 依赖 UserRepository ,所以 RegistrationComponent 必须可以从 AppComponent 中获取对象。使用 Dagger subcomponents 可以告知 Dagger 我们想使用另一个容器的部分模块,创建一个新的容器。这个新的容器(比如:这个例子中的 RegistrationComponent)必须是一个子容器,它包含并共享另一个容器的资源(比如:这个例子中的 AppComponent)。

子容器是继承并扩展于一个父容器的,因此,父容器所提供的所有对象,子容器也可以提供。正因如此,一个子容器中的对象可以依赖父容器中的对象。

由于这里是针对 "registration" 模块,因此我们可以在 registration 包下创建一个名为 RegistrationComponent.kt 的新文件,在这个文件中,我们可以创建一个名为 RegistrationComponent 的新接口。然后使用 @Subcomponent 注解这个接口,以此来告知 Dagger 它就是一个子容器。

registration/RegistrationComponent.kt

package com.example.android.dagger.registration

import dagger.Subcomponent

// Definition of a Dagger subcomponent
@Subcomponent
interface RegistrationComponent {

}
复制代码

这个容器需要包含注册有关的信息,因此,我们需要做如下两点:

  • 把 AppComponent 中有关注册的 inject 方法移到 这个子容器中来(如:RegistrationActivityEnterDetailsFragmentTermsAndConditionsFragment);

  • 创建一个子容器工厂,我们可以使用它来创建子容器的实例。

RegistrationComponent.kt

// Definition of a Dagger subcomponent
@Subcomponent
interface RegistrationComponent {

    // Factory to create instances of RegistrationComponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): RegistrationComponent
    }

    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
    fun inject(fragment: EnterDetailsFragment)
    fun inject(fragment: TermsAndConditionsFragment)
}
复制代码

在 AppComponent ,我们必须删除所有有关注册的注入方法,因为我们不在使用它了,而是使用 RegistrationComponent

此外,为了 RegistrationActivity 可以创建 RegistrationComponent 实例,我们需要在 AppComponent 接口里把这个子容器的工厂暴露出来,代码如下:

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance context: Context): AppComponent
    }

    // Expose RegistrationComponent factory from the graph
    fun registrationComponent(): RegistrationComponent.Factory

    fun inject(activity: MainActivity)
}
复制代码

从上面的代码可以看出,我们可以通过在主容器中定义一个方法,该方法的返回值定义为子容器 RegistrationComponent 的工厂类型,以此来暴露给相关依赖者完成注入。

至此,通过上面的章节内容,我们可以知道业务模块和 Dagger 依赖图有两种相互作用的途径:

  1. 在主容器中声明一个返回值为 Unit (Java 中就是没有返回值)的方法,然后以一个类为作为方法的参数类型,这样就意味着,Dagger 被允许在这个类中实现属性注入。(如:fun inject(activity: MainActivity))

  2. 在主容器中中声明一个带返回值的方法,这样就意味着,可以通过依赖图获取该类型的实体。(如:fun registrationComponent(): RegistrationComponent.Factory)

现在,我们还必须让 AppComponent 知道它有一个子容器 RegistrationComponent ,这样它才可以为子容器生成相关代码。我们需要创建一个 Dagger Module 来实现这一目的。

让我们在 di 包中创建一个名为 AppSubcomponents.kt 的文件,在这个文件里,我们定义一个名为 AppSubcomponents 的类并在这个类上面加入 @Module 注解。为了指定子容器 AppSubcomponents 包含哪些业务模块的依赖,为此我们在 @Module 注解中添加了一个子容器集合类型参数,参数变量名为 subcomponents ,实现代码如下:

// This module tells AppComponent which are its subcomponents
@Module(subcomponents = [RegistrationComponent::class])
class AppSubcomponents
复制代码

这个新的 module 也需要加到 AppComponent 中,实现代码如下:

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent { ... }
复制代码

现在的应用依赖图看起来是什么样子呢?

View classes specific to registration get injected by the RegistrationComponent. Since RegistrationViewModel and EnterDetailsViewModel are only requested by classes that use the RegistrationComponent, they're part of it and not part of AppComponent.

9、子容器的作用域(Scoping Subcomponents)

为了实现在 Activity 以及多个 Fragments 之间共享相同的 RegistrationViewModel 实例,我们创建了一个子容器。正如我们之前在章节 使用 Dagger 容器的作用域(Using Scopes) 中提到的那样,让我们使用一个相同的作用域注解,分别把它加到子容器类 RegistrationComponent 和需要共享的实例的类 RegistrationViewModel 之上,这样就可以实现在子容器的生命周期内,这个实例是唯一的。

然而,因为我们在 AppComponent 中已经使用过 @Singleton ,我们需要创建一个不同名字的作用域注解。

在这个例子中,我能命名这个作用域注解叫做 @RegistrationScope ,但是不是一个好的设计。作用域注解的命名最好不要使用具体的业务来命名。它的命名应该取决于声明周期,这样这个作用域注解就可以被其他有类似声明周期的容器重用(例如:LoginComponentSettingsComponent ,等等)。因此,我们使用 @ActivityScope ,而不是 @RegistrationScope

作用域准则:

  • 当一个类型被作用域注解打上标记,那么它只能够在和这个作用域注解对应的容器中被使用;

  • 当一个容器被作用域注解打上标记,它既可以提供被这个作用域注解的类型,也可以提供没有被这个作用域注解的类型;

  • 一个子容器不能够使用它父容器的作用域注解。

主容器的上下文也包含子容器。

开始编码,然我们在 di 包下面新建 ActivityScope,代码如下:

@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope
复制代码

RegistrationViewModel 指定和 RegistrationComponent 一样的作用域,代码如下:

// Scopes this ViewModel to components that use @ActivityScope
@ActivityScope
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}
复制代码
// Classes annotated with @ActivityScope will have a unique instance in this Component
@ActivityScope
@Subcomponent
interface RegistrationComponent { ... }
复制代码

译者注:

其实后面还有 5 个章节,由于时间关系,就不翻译了,结合示例的源码以及生成的DaggerAppComponent等相关源码,看懂也不算太难

剩下的 5 个章节,大概就是:

  • “重构登录流程” —— 大体就是建议使用 Dagger 容器时不要弄得容器的业务逻辑过于庞大,这样不利于可读和维护,应该酌情细化容器大小

  • “多个 Activity 中使用相同的作用域对象” —— 这里就是以 UserMananger 中的依赖对象 UserDataRepository 为例,使用 Dagger 子容器来重构它的依赖。这个例子其实纯粹是为了讲解 Dagger 的用法,文章并不太推荐,文章称之为 “有条件的属性注入是非常危险的”,这里也引出了,最后一章的内容,需要读者自行完善。

  • “Dagger的单元测试” —— kaptAndroidTest

  • “@Provides 和 限定符” —— @Providers 和 @Binds 类似,但是性能要低;但它可以提供工程之外的类。至于这里的限定符,和 Java 中的 @Name的用途相同,只是 Java 的这个 @Name 不能完全满足 Android 特有的编译器和生产环境,所以 Dagger 库要自行实现了 @Qualifier 类型的注解

以上就是所有内容了,有兴趣的可以去看原文,本文仅作为个人的笔记和温故内容了~~ 可以用于更好的使用 Hilt,打个基础。

文章分类
Android
文章标签