移动应用与桌面应用有本质上的不同------用户每次只能访问一个应用,而且它们往往很小,只专注于一个特定的任务。------ 《安卓传奇 安卓缔造团队回忆录》,Chet Haase。
依赖注入(Dependency Injection,下文以DI简称)是控制反转(Inverse Of Control)思想的一种实践,笔者在早期基于Spring进行后端开发时,就已经尝试使用过类似工具。DI 打破了常规的 "创建对象->使用对象" 顺序,将对象的生成过程进行解耦,赋予软件更大的灵活性,同时也能在功能发生变化/扩展时,将发生变化的部分与不变部分进行分离,减少变更带来的工作量。
1. 什么是依赖注入
要想了解什么是 "依赖注入" ,首先需要理解什么是 "非依赖注入"。
1.1 非依赖注入
在面向对象的编程语言中,"依赖注入"可以分为两个词,"依赖" 和 "注入" 。所谓"依赖",是指两个对象之间存在的 调用关系 ,对象A调用对象B,则可认为A对B有依赖 ,而B对A不存在依赖。在这种场景下,"注入"则描述了"将对象B提供给对象A的方式",是通过"注入"而非直接 new 出来对象。
"非依赖注入"就是最平实的 "创建对象->使用对象" 方式。我们以经典的"汽车"和"发动机"两者关系为例,汽车对象持有发动机对象实例,因此存在 "汽车->发动机" 的依赖关系。

【汽车持有发动机依赖】
kotlin
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
在上述代码中,可以看到Car对象内部 创建并持有 了一个Engine对象,两者耦合在一起,如果未来从"汽油发动机"升级为"电机",则必须对Car类的代码进行修改。这违背了"开放-关闭"(对扩展开放,对修改关闭)的原则。因此,这是一种不利于扩展的设计模式。
1.2 手动依赖注入

【手动依赖注入Engine参数】
既然"发动机"对象未来有变化的可能,那么我们把它作为 构造函数的一个参数 传给Car不就可以了?这便是"手动依赖注入",其代码如下:
kotlin
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
手动依赖注入分为两种方式
- 构造函数注入:如上述代码,对象作为构造函数的参数传入,不支持动态变化
- setter注入 :提供
setter函数,可以随时设置对象
kotlin
// setter注入
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
1.3 现代架构中的手动依赖注入

【AndroidAPP架构】
在 AndroidAPP 架构图中,对象延伸出的箭头指向其依赖的对象,从图中可以看出:
Activity依赖ViewModelViewModel依赖RepositoryRepository依赖DataSource(Model、RemoteDataSource)
以使用 Repository 为例,我们需要手动为其注入 DataSource 实例。
kotlin
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// In order to satisfy the dependencies of LoginViewModel, you have to also
// satisfy the dependencies of all of its dependencies recursively.
// First, create retrofit which is the dependency of UserRemoteDataSource
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
// Then, satisfy the dependencies of UserRepository
val remoteDataSource = UserRemoteDataSource(retrofit)
val localDataSource = UserLocalDataSource()
// Now you can create an instance of UserRepository that LoginViewModel needs
val userRepository = UserRepository(localDataSource, remoteDataSource)
// Lastly, create an instance of LoginViewModel with userRepository
loginViewModel = LoginViewModel(userRepository)
}
}
在 Activity 中,我们需要创建一系列的依赖关系对象,这样会导致项目中出现大量的 样板代码 (Boilerplate code)。想象一下,对于每个 Activity 都需要创建一系列相似的代码,这是一件枯燥并且容易出错的事情。
所以我们思考,是否可以将创建依赖关系的代码抽出来,做成工厂模式呢?
1.4 使用工厂模式进行手动依赖注入

可以设计一个AppContainer类,作为 Repository 对象的容器,在每一个 Activity 需要使用 Repository 时,从该工厂里面获取。
kotlin
// Application 中创建工厂类
// Container of objects shared across the whole app
class AppContainer {
// Since you want to expose userRepository out of the container, you need to satisfy
// its dependencies as you did before
private val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
private val remoteDataSource = UserRemoteDataSource(retrofit)
private val localDataSource = UserLocalDataSource()
// userRepository is not private; it'll be exposed
val userRepository = UserRepository(localDataSource, remoteDataSource)
}
// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {
// Instance of AppContainer that will be used by all the Activities of the app
val appContainer = AppContainer()
}
// Activity 中使用工厂类
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Gets userRepository from the instance of AppContainer in Application
val appContainer = (application as MyApplication).appContainer
loginViewModel = LoginViewModel(appContainer.userRepository)
}
}
更进一步,将 ViewModel 也通过工厂模式生成,同样在 AppContainer 中对其进行管理。
kotlin
// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
fun create(): T
}
// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
class AppContainer {
...
val userRepository = UserRepository(localDataSource, remoteDataSource)
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Gets LoginViewModelFactory from the application instance of AppContainer
// to create a new LoginViewModel instance
val appContainer = (application as MyApplication).appContainer
loginViewModel = appContainer.loginViewModelFactory.create()
}
}
1.5 容器的生命周期管理
在使用容器的过程中,如果只在 Application.onCreate() 时初始化,而不去销毁它,此时该容器的生命周期就是 跟随进程的,是常驻于内存中的实例。在实际应用场景里,并不是所有注入对象都要维持如此之长的生命周期。
- 页面销毁后,注入对象/容器就不再有存在的意义
- 业务逻辑同时存在多个实例,对于注入的类,此时需要生成多个对象
因此,"生命周期管理" 也是进行依赖注入时必须要考虑的问题,在手动依赖注入过程中,可以通过LifecycleObserver,将容器和 Activity/Application 的生命周期绑定。
如下例,在 LoginActivity 中,onCreate()时创建LoginContainer,它用来生成 LoginViewModel,onDestroy() 时则销毁 LoginContainer。
kotlin
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
private lateinit var loginData: LoginUserData
private lateinit var appContainer: AppContainer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appContainer = (application as MyApplication).appContainer
// Login flow has started. Populate loginContainer in AppContainer
appContainer.loginContainer = LoginContainer(appContainer.userRepository)
loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
loginData = appContainer.loginContainer.loginData
}
override fun onDestroy() {
// Login flow is finishing
// Removing the instance of loginContainer in the AppContainer
appContainer.loginContainer = null
super.onDestroy()
}
}
2. 自动依赖注入
终于来到了本文最激动人心的部分------ 使用 Hilt 进行依赖注入。
2.1 搭建 Hilt 环境
我们需要在项目里引入 Hilt,分为两个步骤:
2.1.1 在gradle文件中接入依赖
在项目根目录的 build.gradle 中增加 Hilt 插件。
kotlin
plugins {
...
id("com.google.dagger.hilt.android") version "2.57.1" apply false
}
随后在 主module 的 app/build.gradle 文件里面使用上一步的插件,并声明hilt-android依赖。
kotlin
plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
android {
...
}
dependencies {
implementation("com.google.dagger:hilt-android:2.57.1")
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
}
到这一步,就完成了gradle层面的接入。
2.1.2 在Kotlin类上创建入口点
接下来我们要在使用 Hilt 的地方创建入口点,对于 Android 来说,首当其冲的便是 Application,使用 @HiltAndroidApp 进行注解,这一步实际上是创建了跟随 Application 生命周期的 Hilt 组件,它是应用的根组件,其他 Hilt 组件可以使用它。
kotlin
@HiltAndroidApp
class ExampleApplication : Application() { ... }
在 Activity 中,我们使用 @AndroidEntryPoint 创建入口点。
kotlin
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }
对于Android项目来说,支持在以下地方创建Hilt的入口点。
Application(by using@HiltAndroidApp)ViewModel(by using@HiltViewModel)ActivityFragmentViewServiceBroadcastReceiver
@AndroidEntryPoint实际上是创建了一个独立的Hilt组件,并将其加入到组件树当中。在组件树里面,最上方是根节点的@HiltAndroidApp组件,它向下生长,位于低层的组件可以接收到上层组件管理的对象,整株组件树如下图所示。

2.2 使用自动依赖注入
在完成搭建环境后,就可以在代码里通过注入自动创建依赖了。我们先看使用最多的,即作为构造参数的场景,通过 @Inject constructor 注解,告知系统该构造函数的参数需要通过 Hilt 进行注入。
kotlin
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
在上例中,AnalyticsAdapter 需要依赖 AnalyticsService,此时无需 new AnalyticsService(),而是通过 Hilt 的对象管理机制自动提供。
接下来你可能会问,对于 AnalyticsService,Hilt 又如何知道要如何创建它的实例?即使该类也是通过注解自动生成,但归根结底总会追溯到某个原点,此时就涉及到了 Hilt 中的另一个重要概念------
Module
2.3 module(模块)在Hilt中的作用
在 Hilt 搭建的依赖注入体系中,module 承担了 "创建实例" 的职责------并不是所有的实例都可以通过@Inject constructor注解来自动创建,总有一些实例是无法通过 Hilt 的组件树自动进行构建的,例如那些位于 叶子结点 的类。
此时可以借助 module,完成叶子结点的实例化,有两种写法。
@Binds注解:用于注解在抽象类/接口上面,为该抽象类/接口提供具体实现@Provides注解:用于注解在单例上面,手动创建被注解的对象
kotlin
interface AnalyticsService { // ===> 待注入的接口,下文分别通过@Binds、@Provides提供两种注入方式
fun analyticsMethods()
}
// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor( // ===> AnalyticsServiceImpl.kt类在我们的项目中,可以修改其源码(增加@Inject注解)
...
) : AnalyticsService { ... }
// 首先是@Binds方式
@Module // ===> 所有module必须声明
@InstallIn(ActivityComponent::class) // ===> 注入到Activity组件
abstract class AnalyticsModule { // ===> 抽象类 or 接口
@Binds // ===> 开始注入
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
// 随后是@Provides方式
@Module // ===> 同样需要声明
@InstallIn(ActivityComponent::class) // ===> 同上
object AnalyticsModule { // ===> 注意这里是单例
@Provides
fun provideAnalyticsService(
// Potential dependencies of this type
): AnalyticsService {
return Retrofit.Builder() // ===> 手动创建实例进行注入,生命周期为ActivityComponent
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
2.4 对同一类型采用多种注入实现
在实际的开发进程中,存在这样的场景:对于同一个接口,APP开发阶段使用 mock实现 ,在进入前后端联调时,切换为 后端接口实现。Hilt对此场景也提供了注解的方式,便于进行切换。
kotlin
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient // ===> 注解1拦截器
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient // ===> 注解2拦截器
生成注入对象时,使用上面两个注解,生成不同的OkHttpClient对象。
kotlin
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@AuthInterceptorOkHttpClient // ===> 注解1生成的对象
@Provides
fun provideAuthInterceptorOkHttpClient(
authInterceptor: AuthInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
@OtherInterceptorOkHttpClient // ===> 注解2生成的对象
@Provides
fun provideOtherInterceptorOkHttpClient(
otherInterceptor: OtherInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(otherInterceptor)
.build()
}
}
进行对象注入时,则可以方便地在@AuthInterceptorOkHttpClient 和 @OtherInterceptorOkHttpClient两者之间切换。
kotlin
// As a dependency of another class.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
@AuthInterceptorOkHttpClient okHttpClient: OkHttpClient // ===> module函数参数注入
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.client(okHttpClient)
.build()
.create(AnalyticsService::class.java)
}
}
// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
@AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient // ===> 构造函数参数注入
) : ...
// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {
@AuthInterceptorOkHttpClient // ===> 成员变量注入
@Inject lateinit var okHttpClient: OkHttpClient
}
2.5 可供安装的Android组件(Component)
在上面的例子中,你可能会留意到,声明 @Module 后,又使用了 @InstallIn(ActivityComponent::class),这一行声明的作用是指定该module进行安装的目标,该目标对应着Android系统中的不同组件,这也是作为依赖注入框架,Hilt在Dagger基础上所增加的内容。
Hilt 内置了一系列 Component,如下表所示:
| Hilt component 组件 | Scope 作用域 | Inject for 对应的Android组件 | 创建时机 | 销毁时机 |
|---|---|---|---|---|
| SingletonComponent | @Singleton | Application | Application#onCreate() | Application destoryed |
| ActivityRetainedComponent | @ActivityRetainedScoped | Application | Activity#onCreate() | Activity#onDestroy() |
| ViewModelComponent | @ViewModelScoped | ViewModel | ViewModel created | ViewModel destroyed |
| ActivityComponent | @ActivityScoped | Activity | Activity#onCreate() | Activity#onDestroy() |
| FragmentComponent | @FragmentScoped | Fragment | Fragment#onAttach() | Fragment#onDestroy() |
| ViewComponent | @ViewScoped | View | View#super() | View destroyed |
| ViewWithFragmentComponent | @ViewScoped | View annotated with @WithFragmentBindings | View#super() | View destroyed |
| ServiceComponent | @ServiceScoped | Service | Service#onCreate() | Service#onDestroy() |
组件的生命周期描述了它自己创建/销毁的时机,通过该组件进行注入的对象上,如果没有声明作用域,则每次访问该对象,都会创建一个新的实例 。这样做的好处是即用即抛,可以避免对象泄露。但业务上难免会有维持、共享状态的场景,此时就要为注入实例加上域注解,声明该实例的管理方式。在日常开发中,使用比较多的是单例域 @Singleton 和 Activity 域 @ActivityScoped。
2.6 获取内置注入对象的简化写法
在声明依赖注入时,对于Hilt内置的Android组件,可以直接在构造器中获取它,如下例。
kotlin
// 直接获取 ApplicationContext 对象
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext context: Context
) : AnalyticsService { ... }
// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
application: Application // 也可以省略掉注解,仍然生效
) : AnalyticsService { ... }
// 直接获取Activity对象
class AnalyticsAdapter @Inject constructor(
@ActivityContext context: Context
) { ... }
// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
activity: FragmentActivity // 省略注解的写法
) { ... }
3. 对依赖注入的再思考

【六大设计原则】
依赖注入不仅仅是一项技术、一个框架,更是架构设计思想的具象化 。它将常规的 "创建A,把A传给B" 升级成 "创建B时,使其自动获取A" 。在我看来,它有以下3点优势:
1. 解耦
毫无疑问,自动化依赖注入能够简化对象的创建过程,将对象的依赖关系自动化处理,降低代码耦合。作为开发和维护人员,我们能够自由地替换接口实现,对修改关闭,对扩展开放(开闭原则),有利于软件版本的长期迭代。
2. 生命周期管理
在"依赖倒置"的基础上,Hilt还与Android的生命周期紧密结合,自动生成与Application、Activity 等组件一致的组件实现。在自动提供组件注入的基础上,防止由于生命周期管理不当导致的对象泄露。
举一个我在真实业务中遇到的场景,当时对依赖注入的组件树理解还不透彻,误将一个ActivityContext注入给SingletonComponent,编码时未提示异常,但构建时会报错------生命周期冲突,Hilt可以在构建时自动识别这种泄露问题,为编码安全增加一道保障。
3. 使架构更加清晰
对于使用 Hilt 的项目来说,还有一个并不明显但十分重要的优点,那就是 Hilt 的依赖注入写法,能够让架构分层更加清晰。项目中每一个关键组件的依赖方都可以在依赖关系中一眼看出,这些组件的生命周期也会更加直观地呈现出来。