依赖注入是什么
个人理解:把有依赖关系的类放在容器中,解析这些类的实例,并在运行时注入到对应的字段中,就是依赖注入,目的是为了类的解耦
例子:A 类 中用到了 B 类,一般情况下需要在 A 类中 new B() 的实例对象
采用依赖注入后,在 A 类中 定义一个私有的 B 类 字段。并在运行的时候通过从相关的容器中获取出来 B 的对象并注入到 A 类中的 字段中。
这样做的好处是什么?
如果有很多个类需要使用 B 类。难道都要在各自的类中进行 new B() 吗。这样对后期的维护和管理都是不方便的。使用 依赖注入则就变得很简单了。
HILT 是什么
Hilt 是 Android 的依赖注入库,其实是基于 Dagger 。可以说 Hilt 是专门为 Andorid 打造的。
Hilt 创建了一组标准的 组件和作用域。这些组件会自动集成到 Android 程序中的生命周期中。在使用的时候可以指定使用的范围,事情作用在对应的生命周期当中。
HILT 常用的注解的含义
- @HiltAndroidApp @HiltAndroidApp 将会触发 Hilt 的代码生成,作为程序依赖项容器的基类 生成的 Hilt 依附于 Application 的生命周期,他是 App 的父组件,提供访问其他组件的依赖 在 Application 中配置好后,就可以使用 Hilt 提供的组件了;组件包含 Application,Activity,Fragment,View,Service 等。
- @HiltAndroidApp 创建一个依赖容器,该容器遵循 Android 的生命周期类,目前支持的类型是: Activity, Fragment, View, Service, BroadcastReceiver.
- @Inject 使用 @Inject 来告诉 Hilt 如何提供该类的实例,常用于构造方法,非私有字段,方法中。 Hilt 有关如何提供不同类型的实例信息也称之为绑定
- @Module module 是用来提供一些无法用 构造@Inject 的依赖,如第三方库,接口,build 模式的构造等。 使用 @Module 注解的类,需要使用 @InstallIn 注解指定 module 的范围 增加了 @Module 注解的类,其实代表的就是一个模块,并通过指定的组件来告诉在那个容器中可以使用绑定安装。
- @InstallIn 使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围。 例如使用 @InstallIn(ActivityComponent::class) 注解的 module 会绑定到 activity 的生命周期上。
- @Provides 常用于被 @Module 注解标记类的内部方法上。并提供依赖项对象。
- @EntryPoint
Hilt 支持最常见的 Android 类 Application、Activity、Fragment、View、Service、BroadcastReceiver 等等,但是您可能需要在Hilt 不支持的类中执行依赖注入,在这种情况下可以使用 @EntryPoint 注解进行创建,Hilt 会提供相应的依赖。
HILT 中的组件(COMPENENT)
使用 @Module 注解的类,需要使用 @Installin 注解来指定 module 的范围。
例如 @InstallIn(ApplicationComponent::class) 注解的 Module 就会绑定到 Application 的生命周期上。
Hilt 提供了以下组件来绑定依赖与对应 Android 类的活动范围
Hilt 组件 | 对应 Android 类活动的范围 |
---|---|
ApplicationComponent | Application |
ActivityRetainedComponent | ViewModel |
ActivityComponent | Activity |
FragmentComponent | Fragment |
ViewComponent | View |
ViewWithFragmentComponent | View annotated with @WithFragmentBindings |
ServiceComponent | Service |
Hilt 没有为 broadcast receivers 提供组件,因为 Hilt 直接进从 ApplicationComponent 中注入 broadcast receivers。
HILT 中组件的生命周期
Hilt 会根据相应的 Android 类生命周期自动创建和销毁组件的实例,对应关系如下:
Hilt 提供的组件 | 创建对应的生命周期 | 结束对应的生命周期 | 作用范围 |
---|---|---|---|
ApplicationComponent | Application#onCreate() | Application#onDestroy() | @Singleton |
ActivityRetainedComponent | Activity#onCreate() | Activity#onDestroy() | @ActivityRetainedScope |
ActivityComponent | Activity#onCreate() | Activity#onDestroy() | @ActivityScoped |
FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() | @FragmentScoped |
ViewComponent | View#super() | View destroyed | @ViewScoped |
ViewWithFragmentComponent | View#super() | View destroyed | @ViewScoped |
ServiceComponent | Service#onCreate() | View destroyed | @ViewScoped |
Android常用的依赖注入框架
Dagger
由Square公司开源,基于Java反射去实现的,从而有两个潜在的隐患:
- 反射是比较耗时的,用这种方式会降低程序的运行效率。(这问题不大,现在的程序中到处都在用反射)
- 依赖注入框架的用法总体来说比较有难度,很难一次性编写正确。而基于反射实现的依赖注入功能,在编译期无法得知依赖注入的用法是否正确,只能在运行时通过程序是否崩溃来判断。这样测试的效率低下,容易将一些 bug 隐藏得很深。
Dagger2
由 Google 开发,基于 Java 注解实现的,把 Dagger1 反射的那些弊端解决了:
通过注解,Dagger2 会在编译时期自动生成用于依赖注入的代码,不会增加任何运行耗时。另外,Dagger2 会在编译时检查依赖注入用法是否正确,若不正确则会直接编译失败,从而将问题尽可能早地抛出。即项目正常编译通过,说明依赖注入的用法基本没问题了。
但 Dagger2 使用比较复杂,若不能很好地使用它,可能会拖累你的项目,甚至会将一些简单的项目过度设计。
Hilt
Google 发布了 Hilt,是在依赖项注入库 Dagger 的基础上构建而成,一个专门面向 Android 的依赖注入框架。
相比于 Dagger2,Hilt 最明显的特征就是: 简单、提供了 Android 专属的 API。
Hilt的使用
1、依赖版本配置
项目根build.gradle依赖
ext.kotlin_version = "1.5.0"//kotlin版本
ext.hilt_version = '2.35'//hilt版本
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35'
}
项目app中build.gradle依赖
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
项目模块中build.gradle所使用插件
id 'kotlin-kapt'
id 'kotlin-android-extensions'
2、Hilt应用
1.定义Hilt应用类
- 用@HiltAndroidApp注解Application;所有使用Hilt注入框架的应用都必须包含一个带有 @HiltAndroidApp 注释的 Application 类。
- @HiltAndroidApp注解 会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器;
@HiltAndroidApp
class MyApplication : MultiDexApplication() {
}
然后,在AndroidManifest.xml中引入我们自定义的Application类。此时,生成的Hilt组件会附加到 Application 对象的生命周期,并为其提供依赖项。此外,由于它也是应用的父组件,所以其他组件可以访问它提供的依赖项。
2.创建注入类
为了执行字段注入,需要在类的构造函数中使用 @Inject 注解,以告知 Hilt 如何提供该类的实例
class Car @Inject constructor() {
var name: String = ""
fun run() {
}
}
使用 @Inject 注释执行字段 @AndroidEntryPoint 会为项目中的每个 Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项, 如需从组件获取依赖项,请使用 @Inject 注释执行字段注入, 注意:Hilt注入的字段是不可以声明成private的;
3.依赖项注入 Android 类
用@AndroidEntryPoint注释类 目前支持6类入口点:Application(通过使用 @HiltAndroidApp),Activity,Fragment,View,Service,BroadcastReceiver
使用 @AndroidEntryPoint 注解 Android 类,还必须为依赖于该类的 Android 类添加注释,例如为注解 fragment ,则还必须为该 fragment 依赖的 Activity 添加@AndroidEntryPoint注释。
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
@Inject
lateinit var car: Car
}
注意:带参数的依赖注入,需要构造函数中所依赖的所有其他对象都支持依赖注入;例如:Car的构造函数有Engine,则Engine构造函数也需要@Inject。
3、Hilt模块
有时一些类型参数(如接口或来自外部库的类)不能通过构造函数注入,对于这种情况,我们可以使用Hilt 模块来向Hilt提供绑定信息。
Hilt模块是一个带有@Module注释的类,并使用 @InstallIn 设置作用域。与Dagger模块的作用一样,它会告知Hilt如何提供某些类型的实例。与Dagger模块不同的是,我们必须使用@InstallIn注解为Hilt模块添加注释,以告知Hilt模块将用在哪个Android类中。
1.@Binds注入接口
由于Phone是一个接口,无法通过构造函数注入Hilt,而应向Hilt提供绑定信息。即在Hilt模块内创建一个带有@Binds注释的抽象函数。
通常,带有注释的函数会向Hilt提供以下信息:
- 函数返回类型会告知Hilt函数提供的是哪个接口的实例。
- 函数参数会告知Hilt需要提供哪种实现。
//1. 接口
interface Phone {
fun call()
}
//2. 实现类
class Huawei @Inject constructor() : Phone {
override fun call() {
}
}
//3. 被注入的类,入参是接口类型
class People @Inject constructor(val phone: Phone) {
fun call() {
phone.call()
}
}
//4. 使用@Binds注入接口实例
@Module
@InstallIn(ActivityComponent::class)
abstract class PhoneModel {
@Binds
abstract fun bindPhone(phone: Huawei): Phone
}
//5. 使用注入的实例
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
@Inject
lateinit var people: People
fun test() {
people.phone.call()
}
}
由于PhoneModel 带有 @InstallIn(ActivityComponent.class) 注释,因为我们可以将该依赖项注入Activity中。并且,PhoneModel中的所有依赖项都可以在所有Activity中使用。
2.@Provides注入实例
如果某个类不归您所有(因为它来自外部库,如 Retrofit、OkHttpClient 或 Room 数据库等类),或者必须使用构建器模式创建实例,也无法通过构造函数注入。
这种情况注入方法是在Hilt模块内创建一个函数,然后使用@Provides为该函数添加注释。
带有注释的函数会向 Hilt 提供以下信息:
- 函数返回类型会告知 Hilt 函数提供哪个类型的实例。
- 函数参数会告知 Hilt 相应类型的依赖项。
- 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体。
@Module
@InstallIn(SingletonComponent::class)
class DINetworkModule {
/**
* [OkHttpClient]依赖提供方法
*
* @return OkHttpClient
*/
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
// 日志拦截器部分
val level = if (BuildConfig.VERSION_TYPE != VersionStatus.RELEASE) BODY else NONE
val logInterceptor = HttpLoggingInterceptor().setLevel(level)
return OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.connectTimeout(15L * 1000L, TimeUnit.MILLISECONDS)
.readTimeout(20L * 1000L, TimeUnit.MILLISECONDS)
.writeTimeout(20L * 1000L, TimeUnit.MILLISECONDS)
.addInterceptor(logInterceptor)
// .addInterceptor(CookiesInterceptor())
.build()
}
/**
* 项目主要服务器地址的[Retrofit]依赖提供方法
*
* @param okHttpClient OkHttpClient OkHttp客户端
* @return Retrofit
*/
@Singleton
@Provides
fun provideMainRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(HttpBaseUrlConstant.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
}
}
3.限定符
- 限定符也是一种注解,当为某个类型定义了多个绑定时,我们可以使用它来标识该类型的特定绑定。
- 使用@Qualifier注解实现限定符。
3.1为同一类型提供多个绑定
如果需要让Hilt以依赖项的形式提供同一类型的不同实现,那么必须向 Hilt 提供多个绑定,同一类型定义多个绑定可以使用限定符来实现。
//1. 接口和实现类
interface Phone {
fun call()
}
class Huawei @Inject constructor() : Phone {
override fun call() {
}
}
class Xiaomi @Inject constructor() : Phone {
override fun call() {
}
}
//2. 创建多个类型的注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindHuawei
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindXiaomi
//@Retention:注解的生命周期
//AnnotationRetention.SOURCE:仅编译期,不存储在二进制输出中
//AnnotationRetention.BINARY:存储在二进制输出中,但对反射不可见
//AnnotationRetention.RUNTIME:存储在二进制输出中,对反射可见
//3. 在Hilt模块中使用注解
@Module
@InstallIn(ActivityComponent::class)
abstract class PhoneModel {
@BindHuawei
@Binds
abstract fun bindHuawei(cpu: Huawei): Phone
@ BindXiaomi
@Binds
abstract fun bindXiaomi(cpu: Xiaomi): Phone
}
//4. 使用依赖注入获取实例,可以用在字段注解,也可以用在构造函数或者方法入参中
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
@BindHuawei
@Inject
lateinit var huawei: Phone
@BindXiaomi
@Inject
lateinit var xiaomi: Phone
fun use() {
huawei.call()
xiaomi.call()
}
}
如果需要向某个类型添加限定符,那么应该向提供该依赖项的所有可能的渠道都添加限定符。这是因为让基本的实现或通用的实现在不带限定符的情况下避免出错,也是为了避免导致Hilt注入错误的依赖项。
3.2Hilt中的预定义限定符
Hilt提供了一些预定义的限定符。 Hilt 提供的@ApplicationContext 和 @ActivityContext 限定符用来获取Context 类
class ModuleOne @Inject constructor(@ApplicationContext private val context: Context)
class ModuleTwo @Inject constructor(@ActivityContext private val context: Context)
对于Application和Activity这两个类型,Hilt也是给它们预置好了注入功能(必须是这两个,即使子类也不可以)
class ModuleOne @Inject constructor(val application: Application)
class ModuleTwo @Inject constructor(val activity: Activity)
以上是Android开发中的Jetpack Hilt依赖注入学习,在Android开发进阶中还有大量的核心技术《Android核心技术手册》全部整理在这份文档中。上千个知识点30多个板块笔记,需要的可以参考学习进阶。
文末
HILT 好处
- 降低 Android 开发者使用依赖注入框架的上手成本
- 内部有一套标准的组件和作用域,对范围进行声明后,只能使用在指定的作用域中使用这个类,并且提供声明周期的管理,会自动释放不在使用的对象,减少资源的过度使用,提供代码的可重用性。
- 使用起来简单,告别繁琐的 new。。。 这种流程,只需要添加注解即可。提高了代码的可读性,构建简单,耦合变低,容易测试
- 我感觉最大的好处就是管理他们的生命周期,只能在对应的范围内进行使用。感觉非常好。