一、控制反转原则
谈到依赖注入,不得不提控制反转IoC,那么什么是 IoC ?简单的说 Inversion of Control 是面向对象编程中的一种原则、思想,其主要目的是为了降低模块与模块之间的耦合;通过 第三方 或者 容器 将模块之间的依赖关系解耦。
以汽车 Car 为例,汽车离不开引擎 Engine ,那么通常的实现方法可以是这样:
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
这里的 Car 内部需要提供引擎 Engine 才能运转,也即称 Car 依赖于引擎 Engine 。它们之间是强耦合的关系,假设此时需要换别的引擎如从 V6 升级到 V8 ,内部的实现必须推到重新实现。强耦合导致很不灵活,单测也不方便。引入 Ioc容器 ,将引擎的提供能力实现在 IoC容器 内部提供给 Car ,这种思想称为 控制反转 。而 依赖注入DI 则是基于这种思想所演变的一种设计模式。
二、Android中的依赖注入
项目中为什么需要依赖注入?依赖注入有什么好处呢?
- 有利于代码重用
- 易于对代码进行重构
- 易于单元测试
单个类的功能一般设计得比较单一,一个完整的系统需要多个类多个对象之间的配合才能完成。类引入其他类通常有几种方式:
- 以变量的形式内部实例化所需要的对象如汽车的例子 Car 内部实现所需对象 new Engine() 。
- 从容器中获取,如管理类 Manager 等等。
- 以参数的形式提供,如在构造函数中传入;通过 setter 函数传入,而这种就是一般理解的 依赖注入 。
依赖注入的示例:
//构造函数的形式
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
//setter函数的形式
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
无论是以构造函数的形式还是 setter 函数的形式,都属于手动注入。比较简单的类很好处理,但是在实际的开发过程中,随着项目的日益复杂,依赖项也会越来越多。这种手动注入的形式显然是不能够满足需求的。又或者初始化的流程较长,时间复杂度较高,生命周期的管理与资源的释放都要考虑周全。借助第三方库可以很好地解决此类问题,一般的解决方案主要分为两类:
基于反射的解决方案,可在运行时连接依赖项。
静态解决方案,可生成在编译时连接依赖项的代码。
大名鼎鼎的Dagger就是很优秀的依赖注入库,但是真正用起来的人确很少,一方面 Dagger 太过于优秀,要完全耐心的理解下来需要花时间与精力。另一方面,规则过多,开发者基本都是从 入门到放弃 。而 Hilt 则是在基于 Dagger 的基础实现类一套专为 Android 使用的依赖注入库,简化了使用流程。
三、Hilt的使用
1.作用
Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。
2.依赖包的导入
在项目的根级别文件build.gradle中添加:
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
在app级别的build.gradle文件中添加相关依赖:
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
//需要Android Studio为4.0及以上版本
四、字段含义
1.@HiltAndroidApp
使用 Hilt 作为依赖注入,必须在 Application 类中加入此注释。会触发 Dagger 组件的生成(Hilt底层基于Dagger实现),生成的基类作为容器,主要作用是负责将成员注入到 Android 类中,并在正确的生命周期处实例化组件。与之类似的是 @AndroidEntryPoint 。
@HiltAndroidApp
class CustomApplication : Application() { ... }
组件模块生成后,其绑定就可以用作该组件中其他绑定的依赖项,也可以用作组件层次结构中该组件下的任何子组件中其他绑定的依赖项:
2.@AndroidEntryPoint
被标记的 Class 可以引入对其他组件的支持,简单的说就是引入需要依赖的对象。而不需要在内部通过创建或者其他方式来实例化所需的对象。但是有个前提是,依赖项与被依赖项都需要加上此注释。以 Fragment 与 Activity 之间的关系为例, Fragment 依附于 Activity 之上。即 Fragment 和 Activity 都需要添加此注释 @AndroidEntryPoint 。目前版本能够支持此注释的类有:
- Application(通过使用 @HiltAndroidApp)
- 支持扩展 ComponentActivity 的 Activity,如 AppCompatActivity
- 仅支持扩展 androidx.Fragment 的 Fragment,不支持保留的Fragment
- 支持View、Service、与广播BroadcastReceiver
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {...}
@AndroidEntryPoint
class LogsFragment : Fragment() {...}
//LogsFragment依附于MainActivity,必须同时添加注释@AndroidEntryPoint
3.@Inject
具体实例的注入,以官方Demo为例,简单的 Log 保存与展示,需要 logger 对象来执行相应的操作。可以通过此方式添加依赖(不支持私有属性,即需要public来修饰)。通过注解 @Inject 来“引入”实例对象。
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
}
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
这里的 LoggerDataSource 被设计成接口,这样做有什么好处呢?面向接口编程,为了使扩展更加灵活,设计成接口的形式,可以提供不同的实现方式。例如保存在内存中、存储一些Log保存到数据库中上传到服务端。简单点,以保存到内存中为例,保存到内存的具体实现类 LoggerInMemoryDataSource , Hilt 知道需要一个 LoggerDataSource 对象,但是我们需要告诉 Hilt 这个实例的实现规则。因此在 LoggerInMemoryDataSource 的构造函数一样需要增加 @Inject 。
//构造函数中增加@Inject
class LoggerInMemoryDataSource @Inject constructor() : LoggerDataSource {
private val logs = LinkedList<Log>()
override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}
override fun getAllLogs(callback: (List<Log>) -> Unit) {
callback(logs)
}
override fun removeLogs() {
logs.clear()
}
}
4.@Module与@InstallIn
顾名思义,即模块注入,并且这两个注释都是同时出现的。模块化的开发已经不是陌生的东西了,当构造函数的入参为接口时、外部第三方类。 Hilt 并不知道需要注入的对象类型,这个时候就需要借助 Module 的帮助了,由开发者告诉 Hilt 注入的规则与信息。同时作用域也是必要信息(@InstallIn),比如网络请求、全局的 Toast 这些组件的是 Application 唯一。而一些其他的组件仅仅在 Fragment 被使用。因此模块的定义同样需要告诉 Hilt 应该被设计成全局的还是仅仅是某些页面相关的。
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {...}
如上述全局的数据库模块,首先整个 Application 唯一实例(@InstallIn(SingletonComponent::class)), @Module 字段表示这是一个模块组件。@InstallIn对应关系,如果注入的是接口,那么如何告诉 Hilt 处理呢?这个时候需要借助 @Binds 来告诉Hilt需要提供哪一种具体的实现
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
- LoggerDataSource是定义的接口,LoggingInMemoryModule作为模块来告诉Hilt提供具体实例的规则。
- 通过@Binds来告诉调用者,具体的实现类为LoggerInMemoryDataSource
这里的是接口的注入,但是接口还是由开发者自己定义的,也即是我们是知道具体实现。如果是第三方库,像网络请求、数据库的构建等就需要另一个关键 @Provides ,在模块中直接构造出需要的实例,并提供出去。如全局数据库的构建:
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
而对于接口是存在多种实现的情况的,那么Hilt如何区分到底是需要提供哪个实现类呢?直接给个“标记”,使用时根据不同的标记来提供对应的实例, @Qualifier 限定符允许开发者自定义属性标记,对不同的实现打上不同比较。如记录 Log 的情况,分为本地数据库保存和内存保存。则对应的 Module 可以实现为:
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
//使用时添加不同注释
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
总结一下,Hilt模块:
- 解决注入参数为接口的情况,需要模块来定义接口的具体实现并提供给调用者,需要配合 @Binds
- 解决注入参数为第三方库,或者不属于自身定义的类型,需要模块实现具体的实例并提供给调用者,需要配合 @Provides
- @Binds – 函数返回类型会告知 Hilt 函数提供哪个接口的实例
- @Binds – 函数参数会告知 Hilt 要提供哪种实现
- @Provides – 函数返回类型会告知 Hilt 函数提供哪个类型的实例
- @Provides – 函数参数会告知 Hilt 相应类型的依赖项
- @Provides – 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体
- @Qualifier – 自定义属性,为不同的实现提供对应的标记
五、Hilt组件与Android App组件作用域
六、作用域与生命周期
七、文档
依赖注入DI
Code
Hilt
Github