0%

Android-Dagger2之基础用法

概述

Dagger 的名字取自有向无环图 DAG (directed acyclic graph),因为程序里的依赖关系拼接起来就是一个或者多个有向无环图。

首先理解一下什么是依赖注入。一个类 UserRepository 中有一个 UserRemoteDataSource 类型的属性, 那 UserRemoteDataSource 便是 UserRepository 的依赖,初始化这个依赖可以有两种方法,一种是在类内部自己初始化,另一种是由外部初始化(即是依赖注入,关键在于初始化是谁做的)。

这种由外部初始化的方式都可以叫做依赖注入,而 Dagger 则为依赖注入提供了一种更简单的方式。

基础用法

接下来用 Dagger2 实现如下图所示的依赖关系:

首先添加 Dagger2 的依赖:

1
2
3
4
5
6
apply plugin: 'kotlin-kapt'

dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}

然后定义 UserRepository 类:

1
2
3
4
5
6
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) {
fun load(): String = "${localDataSource.load()} - ${remoteDataSource.load()}"
}

这里通过在构造器上添加 @Inject 注解告知 Dagger 如何创建 UserRepository 实例,它的依赖项为 UserLocalDataSource 和 UserRemoteDataSource, 为了让 Dagger 知道如何创建这两个依赖项实例,也需使用 @Inject 注解:

1
2
3
4
5
6
7
8
9
10
11
interface DataSource {
fun load(): String
}

class UserRemoteDataSource @Inject constructor() : DataSource {
override fun load(): String = "UserRemoteDataSource.load()"
}

class UserLocalDataSource @Inject constructor() : DataSource {
override fun load(): String = "UserLocalDataSource.load()"
}

这样 Dagger 便会知道如何创建它们。

然后在 ViewModel 中也可以通过这种方式依赖 UserRepository:

1
2
3
class LoginViewModel @Inject constructor(private val userRepository: UserRepository) {
fun loadData(): String = userRepository.load()
}

接下来在 Activity 中怎么引用 LoginViewModel 实例呢?显然它不能跟上面一样在构造方法中使用 @Inject 注解了,因为 Activity 实例化是由系统完成的,无法由 Dagger 去进行实例化,因此这里不能由构造函数注入 LoginViewModel, 而应该使用属性注入(注意:注入的属性不能为私有属性):

1
2
3
4
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel
}

光是这样显然是不够的,我们还需要告知 Dagger 要求注入依赖项的对象是谁(LoginActivity),在此之前就得先看看 Dagger Component 的概念。

Component

Dagger 可以在项目中创建一个有向无环图(依赖关系),然后它可以从该图中了解在需要这些依赖项时从何处获取它们。为了让 Dagger 执行此操作,我们需要创建一个接口,并使用 @Component 为其添加注解,在上面的基础用法里,Dagger 需要知道 LoginActivity 中的 LoginViewModel 依赖应该怎么创建:

属性注入

对于属性注入,Dagger 需要让 LoginActivity 访问该图才能提供所需的 LoginViewModel 依赖实例,为此应在 Component 接口中提供一个函数,让该函数将请求注入的对象作为参数。

1
2
3
4
@Component
interface ApplicationComponent {
fun inject(activity: LoginActivity)
}

@Component 会让 Dagger 生成一个容器,其中应包含满足其提供的类型所需的所有依赖项,这称为 Dagger 组件,它包含一个图,其中包括 Dagger 知道如何提供的对象及其各自的依赖项。在 build 项目时 Dagger 会生成 ApplicationComponent 接口的实现:DaggerApplicationComponent。Dagger 通过其注解处理器(处理 @Inject 等)创建了一个依赖关系图,包含了这些依赖之间的关系,并且将 ApplicationComponent 接口中我们加入的方法(inject 方法,方法名可以随意)涉及的依赖作为入口点。

在声明了 inject 方法后,我们可以这样实现注入:

1
2
3
4
5
6
7
8
9
10
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel

override fun onCreate(savedInstanceState: Bundle?) {
DaggerApplicationComponent.create().inject(this)
super.onCreate(savedInstanceState)
println(viewModel.loadData())
}
}

构造器注入

在上面的 LoginActivity 中,如果不通过属性注入 LoginViewModel 依赖的话,还可以通过构造器注入的方式,这里需要在 @Component 接口内定义返回所需类(即 LoginViewModel)的实例的函数。

1
2
3
4
@Component
interface ApplicationComponent {
fun viewModel(): LoginViewModel
}

然后在 LoginActivity 中给 viewModel 赋值:

1
2
3
4
5
6
7
8
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel = DaggerApplicationComponent.create().viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println(viewModel.loadData())
}
}

Module

除了 @Inject 之外,还有一种方法可告知 Dagger 如何提供类实例 —— @Module@Provides

假设将上例中的 UserRemoteDataSource 构造方法改一下,其依赖于 UserService 类:

1
2
3
4
5
6
7
8
9
10
11
class UserRemoteDataSource @Inject constructor(private val userService: UserService) : DataSource {
override fun load(): String = userService.load()
}

class UserService private constructor() {
class Builder {
fun build(): UserService = UserService()
}

fun load(): String = "UserService.load()"
}

可以看到 UserService 对外的创建方法不是通过构造函数,而是通过其 Builder 类来实例化的,那这里就不能使用 @Inject 来进行注入依赖了,在实际项目中这样的场景也不少,我们可以使用 @Module 注解来定义 Dagger 模块,使用 @Provides 注解定义依赖项:

1
2
3
4
5
6
7
8
9
10
11
12
@Module
class NetworkModule {
@Provides
fun provideUserService(): UserService {
return UserService.Builder().build()
}
}

@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
fun viewModel(): LoginViewModel
}

上面的 Module 注解用来告知 Dagger 这是一个 Module 类,而 Provides 注解用来告诉 Dagger 怎么创建该方法返回的类型的实例。这里如果要创建的 UserService 实例需要依赖其他对象,可以将依赖作为方法参数传入,当然该依赖也需要使用 Inject 等注解来告知 Dagger 怎么去创建它。

作用域限定

在上面通过 DaggerApplicationComponent.create().viewModel() 创建的 ViewModel 实例,同一个 DaggerApplicationComponent 对象调用多次 viewModel() 方法,Dagger 默认会创建多个不同的 ViewModel 实例。可以验证:

1
2
val component = DaggerApplicationComponent.create()
println(component.viewModel() == component.viewModel()) // false

如果需要让 Dagger 中同一个 DaggerApplicationComponent 对象始终返回同一 viewModel 实例,可以使用作用域注解(@Singleton)将某个对象的生命周期限定为其 Component 对象的生命周期,这意味着每次获取时都会使用该依赖项的同一实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Singleton
@Component
interface ApplicationComponent {
fun viewModel(): LoginViewModel
}

@Singleton
class LoginViewModel @Inject constructor(private val userRepository: UserRepository) {
fun loadData(): String = userRepository.load()
}

val component = DaggerApplicationComponent.create()
println(component.viewModel() == component.viewModel()) // true

@Singletonjavax.inject 软件包随附的唯一一个作用域注解,也可以创建并使用自定义作用域注解,使用方式跟 @Singleton 一样:

1
2
3
4
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

也可以对 Module 中提供的依赖使用与 Component 一样的作用域注解,这样 Provides 方法中返回的依赖也就与 Component 对象有了一样的生命周期了:

1
2
3
4
5
6
7
8
@Module
class NetworkModule {
@Singleton
@Provides
fun provideUserService(): UserService {
return UserService.Builder().build()
}
}

注意:使用作用域注解的依赖只能在带有相同作用域注解的 Component 中使用

在 Android 中我们通常会创建一个位于 Application 类中的 Dagger 图,这样就将该图附加到应用的生命周期里了:

1
2
3
class MainApplication : Application() {
val applicationGraph = DaggerApplicationComponent.create()
}

于是上面与该 ApplicationComponent 使用同一 @Singleton 注解的依赖项也将获得与其一致的生命周期,通过该 ApplicationComponent 对象中的方法获取到的依赖对象(applicationGraph.viewModel()等),其生命周期将与其一致

作用域限定规则

  • 如果某个依赖类标记有作用域注解,该类型就只能由带有相同作用域注解的组件 Component 使用。
  • 如果某个组件 Component 标记有作用域注解,该组件就只能提供带有该注解的类型或不带注解的类型。
  • 子组件(后面会讲到)不能使用其某一父组件使用的作用域注解。

Subcomponent

为什么使用子组件

上面我们在 MainApplication 应用类中创建了一个 DaggerApplicationComponent 实例,当通过这个 applicationGraph 实例去获取依赖的时候,对于使用 Singleton 注解的依赖(注意 Component 也需要用 Singleton 注解),则其会跟应用生命周期一致(蕾丝于全局单例)。

如需将 LoginViewModel 的作用域限定为 LoginActivity 的生命周期,我们可以在 LoginActivity 中创建 DaggerApplicationComponent 实例属性,然后通过这个实例去获取 LoginViewModel 对象,由于此时这个 DaggerApplicationComponent 实例的生命周期跟 LoginActivity 绑定,则 LoginViewModel 的作用域也会被限定为 LoginActivity 的生命周期:

1
2
3
4
5
6
7
8
9
10
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel

override fun onCreate(savedInstanceState: Bundle?) {
DaggerApplicationComponent.create().inject(this)
super.onCreate(savedInstanceState)
println("Current: $viewModel, ${viewModel.loadData()}")
}
}

通过上面这个方式,可以将 LoginViewModel 的作用域限定为 LoginActivity 的生命周期,但由于 DaggerApplicationComponent 被定义为应用全局的依赖图,而这里我们所需的依赖仅是 LoginActivity 相关的,且又要限制 LoginViewModel 的作用域,为此又在 LoginActivity 中反复创建 DaggerApplicationComponent 实例,显得不那么好看。而且 LoginViewModel 的依赖应该只有登陆这些场景才会使用,可以避免将其放到应用全局的依赖图里。因此这里可以考虑使用子组件(子依赖图),应用组件 DaggerApplicationComponent 中只放一些共有的依赖图,不同的应用场景考虑增加子图,子图存放特有应用/业务场景的依赖,如 LoginViewModle 就放在登陆子图 LoginComponent 中

子组件是继承并扩展父组件的对象图的组件,因此,父组件中提供的所有对象也将在子组件中提供,这样子组件中的对象就可以依赖于父组件提供的对象(共有),父组件向子组件提供的对象的作用域仍限定为父组件的生命周期。

创建子组件

1、定义子组件

1
2
3
4
5
6
7
8
9
10
@Subcomponent
interface LoginComponent {
// 定义子组件 Factory 使得 ApplicationComponent 知道如何创建 LoginComponent 的实例
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}

fun inject(loginActivity: LoginActivity)
}

2、创建子组件的模块

1
2
3
@Module(subcomponents = [LoginComponent::class])
class SubcomponentsModule {
}

3、将声明子组件的模块添加到 ApplicationComponent 中

1
2
3
4
5
6
7
8
9
10
@Singleton
@Component(modules = [NetworkModule::class, SubcomponentsModule::class])
interface ApplicationComponent {
// 告知 Dagger 如何创建 LoginComponent.Factory
// 而 LoginComponent.Factory 会用来创建子组件 LoginComponent 实例
fun loginComponent(): LoginComponent.Factory

// 将 UserRepository 依赖放入 ApplicationComponent 图中
fun repository(): UserRepository
}

注意,这里 ApplicationComponent 之前的 inject 方法删除了,因为 inject 放到了专门的登陆子组件中。增加了一个返回 UserRepository 实例的 repository() 方法,如果不加这个方法且 UserRepository 没有跟 ApplicationComponent 一样用 Singleton 注解标识的话,UserRepository 就会被包含到 LoginComponent 依赖图中,因为它被 LoginViewModel 引用了,这是目前使用它的唯一位置,而 LoginViewModel 是 LoginComponent 子组件中的依赖。现在的依赖关系如下图:

4、使用

1
2
3
4
5
6
7
8
9
10
11
12
13
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel

lateinit var loginComponent: LoginComponent

override fun onCreate(savedInstanceState: Bundle?) {
loginComponent = (application as MainApplication).applicationGraph.loginComponent().create()
loginComponent.inject(this)
super.onCreate(savedInstanceState)
println("Current: $viewModel, ${viewModel.loadData()}")
}
}

LoginComponent 是在 Activity 的 onCreate() 方法中创建的,将随着 Activity 的销毁而被隐式销毁。这样它就具备了跟 LoginActivity 一致的生命周期了。

作用域限定

可以创建自定义作用域注解,并使用该作用域为 LoginComponent 和 LoginViewModel 添加注解。此时如果存在两个需要 LoginViewModel 依赖的 Fragment 页面,则我们在这两个 Fragment 中注入的 LoginViewModel 都是同一个实例(注意不能使用 Singleton 注解,因为其已经被父组件占用了)。

可以将该注解命名为 ActivityScope, 这里不使用 LoginScope, 因为该注解也可以被其他蕾丝的场景(同级组件)使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

@ActivityScope
@Subcomponent
interface LoginComponent {
// 定义子组件 Factory 使得 ApplicationComponent 知道如何创建 LoginComponent 的实例
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}

fun inject(loginActivity: LoginActivity)
fun inject(fragment: LoginAFragment)
fun inject(fragment: LoginBFragment)
}

@ActivityScope
class LoginViewModel @Inject constructor(private val userRepository: UserRepository) { ... }

然后在这两个 Fragment 中可以注入 LoginViewModel 实例:

1
2
3
4
5
6
7
8
9
class LoginAFragment: Fragment() {
@Inject
lateinit var loginViewModel: LoginViewModel

override fun onAttach(context: Context) {
super.onAttach(context)
(activity as LoginActivity).loginComponent.inject(this)
}
}

组件依赖

一个 Component 可以通过设置 dependencies 依赖另一个 Component 组件,然后它便可获取到所依赖的 Component 中暴露的依赖

我们首先创建一个零件组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Singleton
class Memory @Inject constructor() {
override fun toString(): String = "Memory"
}

@Singleton
class Cpu @Inject constructor() {
override fun toString(): String = "Cup"
}

@Singleton
@Component
interface PartComponent {
fun cup(): Cpu
fun memory(): Memory
}

这里使用 Singleton 注解将 Memory 和 Cpu 限制到 PartComponent 零件组件的作用域里。

接着假设一个 Computer 类需要依赖 Cpu 和 Memory:

1
2
3
4
5
6
7
@ActivityScope // 看实际场景,可加可不加
class Computer @Inject constructor(
private val cpu: Cpu,
private val memory: Memory
) {
override fun toString(): String = "$cpu & $memory"
}

然后定义一个依赖 PartComponent 组件的 ComputerComponent 组件:

1
2
3
4
5
6
7
// 需要加作用域,在新版本 Dagger 中没加作用域的话,依赖的 PartComponent 组件如果有作用域注解则会编译报错
// ComputerComponent (unscoped) cannot depend on scoped components
@ActivityScope
@Component(dependencies = [PartComponent::class])
interface ComputerComponent {
fun inject(activity: MainActivity)
}

在这里 ComputerComponent 依赖 PartComponent 组件,因此它可获取到 PartComponent 接口中暴露的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
class MainActivity : AppCompatActivity() {
@Inject
lateinit var computer: Computer

override fun onCreate(savedInstanceState: Bundle?) {
val partComponent = DaggerPartComponent.create()
val computerComponent = DaggerComputerComponent.builder().partComponent(partComponent).build()
computerComponent.inject(this)
super.onCreate(savedInstanceState)
println(computer) // Cup & Memory
}
}

这里我们如果将 PartComponent 接口中暴露依赖的两个方法删掉,那么 ComputerComponent 便不能访问到 Cpu 和 Memory 了,编译报错,因为它会直接尝试去找 Computer 的两个依赖,然后发现这两个依赖被 Singleton 注解标识了,而 ComputerComponent 自身是 ActivityScope 作用域的,因此编译报错(之前我们说依赖如果有作用域注解的话,其作用域注解要和组件的一致)。

关于组件依赖还可举例,依旧用上面的 Cpu 和 Memory 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cpu {
override fun toString(): String = "Cup"
}

class Memory {
override fun toString(): String = "Memory"
}

@Module
class PartModule {
@Provides
fun provideCpu(): Cpu = Cpu()

@Provides
fun provideMemory(): Memory = Memory()
}

这里我们通过 Module 的方式提供依赖。接着在 PartComponent 组件中声明模块,并暴露出获取依赖的方法。

1
2
3
4
5
@Component(modules = [PartModule::class])
interface PartComponent {
fun cup(): Cpu
fun memory(): Memory
}

然后跟上面一样让 ComputerComponent 去依赖 PartComponent 并注入 Computer,这里可以视情况增加作用域限定。

总结

Google 在 Jetpack 包中增加了 Hilt 组件,它是专门针对 Android 平台做的一个依赖注入库,底层依旧是基于 Dagger, 因此学习 Dagger 还是挺有必要的。Hilt 并不是对 Dagger 做了优化,而是针对 Android 开发制定了一套场景化的规则,刚学习了 Dagger 的一些用法,后续有时间接着学习一下 Hilt 及其实现原理。

Dagger 的用法会比较复杂一点,这篇文章结合了我自己的一些理解和用例,讲解了一下 Dagger 的基础用法。文中内容如有错误欢迎指出,共同进步!觉得不错的留个再走哈~

参考: https://developer.android.com/training/dependency-injection/dagger-android