「Jetpack - Hilt依赖注入」

「Jetpack - Hilt依赖注入」

本文已参与「新人创作礼」活动,一起开启掘金创作之路

一、控制反转原则

谈到依赖注入,不得不提控制反转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() { ... }
复制代码

组件模块生成后,其绑定就可以用作该组件中其他绑定的依赖项,也可以用作组件层次结构中该组件下的任何子组件中其他绑定的依赖项:

WechatIMG78.png

2.@AndroidEntryPoint

被标记的Class可以引入对其他组件的支持,简单的说就是引入需要依赖的对象。而不需要在内部通过创建或者其他方式来实例化所需的对象。但是有个前提是,依赖项与被依赖项都需要加上此注释。以FragmentActivity之间的关系为例,Fragment依附于Activity之上。即FragmentActivity都需要添加此注释 @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保存到数据库中上传到服务端。简单点,以保存到内存中为例,保存到内存的具体实现类LoggerInMemoryDataSourceHilt知道需要一个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组件作用域

hilt组件对应关系.png

六、作用域与生命周期

hilt生命周期.png

七、文档

依赖注入DI

Code

Hilt

Github

猜你喜欢

转载自juejin.im/post/7067133366004350984