一、概念
类中使用的某个对象不是在这个类中实例化的(如Activity无法手动实例化使用),而是通过外部注入(从外部传入对象后使用),这种实现方式就称为依赖注入 Dependency Injection(简称DI)。
构造注入 | 将对象B通过构造传参给classA。 | 有些对象无法通过实例化使用,如Activity。 |
字段注入 | 将对象C通过函数设置给classA的字段(也叫setter注入、属性注入)。 | 如果类的依赖项非常多,而且要严格执行顺序(如造车前要造好轮子,造轮子又需要先造好螺丝和轮胎),随着项目越发复杂需要编写很多模板代码耦合度也更高,手动注入就容易出错。 |
方法注入 | 将对象D传入到classA的方法中,仅在该方法中使用。 | |
工厂注入 | ClassA调用工厂类生产对象 | 调用和生产不在同一个地方,不利于修改测试。 |
单例注入 | ClassA调用单例类获取其持有的对象 | 对象的生命周期难以管理,通常并不需要存在于整个APP生命周期,指定在特定的生命周期又需要添加很多判断。 |
自动注入 | 基于反射的解决方案,可以在运行时连接依赖项 | 过多使用反射方法会影响程序的运行效率,而且反射方法在编译阶段是不会产生错误的,导致只有在程序运行时才可以验证反射方法是否正确。Square开发的Dagger。 |
静态解决方案(通过注解),可生成在编译时连接依赖项的代码 | 在编译时就可以发现依赖注入使用的问题。谷歌基于Dagger开发出Dagger2和Hilt,Dagger2使用繁琐,而Hilt专门面向Android开发提供更简单的实现方式,和其它Jetpack组件能更好的协同工作。 |
二、添加依赖
最新版本
2.1 Project.gradle
plugins {
id 'com.google.dagger.hilt.android' version "2.44" apply false
}
2.2 app.gradle
plugins {
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.44'
kapt 'com.google.dagger:hilt-compiler:2.44'
}
// Allow references to generated code
kapt {
correctErrorTypes true
}
三、注解 Application(@HiltAndroidApp)
必须自定义一个Application,并为其添加 @HiltAndroidApp 注解,会触发 Hilt 的代码生成。生成的这一 Hilt 组件会附加到 Application 对象的生命周期,并为其提供依赖项。此外它也是应用的父组件,这意味着其他组件可以访问它提供的依赖项。
四、注入 Android 类(@AndroidEntryPoint)
使用 @AndroidEntryPoint 对以下几种 Android 类添加注解后,就可以向它里面的字段注入依赖了。
- 为某个 Android 类添加注解,则必须为依赖于该类的其它 Android 类添加注解(例如为 FragmentA 添加注解则必须为所有使用该 FragmentA 的 Activity 添加注解)。
目前支持的 Android 类 | 使用的注解 | 说明 |
Activity | @AndroidEntryPoint | 仅支持扩展 ComponentActivity 的 Activity(如AppCompatActivity)。 |
Fragment | 仅支持扩展 androidx.Fragment 的 Fragment,不支持保留的 fragment。 | |
View | ||
Service | ||
BroadcastReceiver | ||
ViewModel | @HiltViewModel |
五、字段注入(@Inject)
声明一个延迟初始化(lateinit var)的属性并添加 @Inject 注解。
- 注入的字段不能为 private 会导致编译错误。
@AndroidEntryPoint
class LoginFragment : Fragment() {
//属性未手动初始化,依赖注入提供了实例,所以不会报错
@Inject lateinit var logBean: LogBean //不能为private
}
六、绑定依赖项 - 构造可被注解的情况
向 Hilt 告知如何创建被依赖类型的实例。为被依赖类型的构造函数添加 @Inject 注解,若构造函数有参数,参数类型的构造函数也都要被注解。
//无参
class LogBean @Inject constructor() {}
//有参
data class LogBean @Inject constructor(
val userName: String, //又依赖了String类型,String的构造也必须被注解
val time: TimeBean //又依赖了TimeBean类型,TimeBean的构造也必须被注解
)
七、绑定依赖项 - 构造无法被注解的情况
向 Hilt 告知如何创建被依赖类型的实例。被依赖类型的构造函数我们无法添加注解(无构造的接口类型、不属于自己的类型如String、Retrofit),就需要手动创建一个模块 Module 并通过函数提供实例。
- 绑定的作用域必须与其安装到的组件的作用域一致,否则在运行程序时会发生异常。
注解模块类 | @Module | 告知 Hilt 如何提供该类型的实例。 |
@InstallIn | 告知 Hilt 模块将用在哪些 Android 类。 | |
注解函数 | @Binds | 提供接口实例。必须对抽象函数注解所以类也是抽象的。返回值告知提供哪种接口类型的实例,参数告知该接口的实现类型(该类型也需要对构造注释)。 |
@Provides | 提供实例。可以对class注解,若只包含@provides函数定义为object更高效。返回值告知提供哪种类型的实例,参数告知提供的实例还依赖了哪些类型(这些类型也需要对构造注释),函数体告知如何创建实例(每当需要提供实例时都会执行函数体)。 |
7.1 提供单个实例
7.1.1 提供接口实例 @Binds
interface IWork
class WorkImpl : IWork
@Module
@InstallIn(ActivityComponent::class)
abstract class WorkModule {
@Binds
abstract fun bindIWork(workImpl: WorkImpl): IWork
}
7.1.2 提供单个实例 @Provides
@Module
@InstallIn(ActivityComponent::class)
object RetrofitModule {
@Provides
fun provideRetrofit(okHeepClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHeepClient)
.baseUrl(ApiService.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
7.2 提供多个不同实现的实例
实际开发中可能需要创建同类型的多个不同实现的对象使用,如 Student("张三") 和 Student("李四")、String("A") 和 String(“B”)。上面的方式只能为目标类型提供相同实现的对象,通过使用限定符来实现区分不同实现。
7.2.1 使用 @Named
只需要对 @bind 或 @Provids 注解的函数再使用 @Named 注解,通过传入唯一的 tag 来区分,使用时也要加入对应 tag 让 Hilt 注入的时候选择对应的实例。
@Module
@InstallIn(ActivityComponent::class)
object StringModule {
@Provides
@Named("One")
fun providesOneString() = "One"
@Provides
@Named("Two")
fun providesTwoString() = "Two"
}
@AndroidEntryPoint
class DemoFragment : Fragment() {
@Inject @Named("One") lateinit var oneString: String
@Inject @Named("Two") lateinit var twoString: String
}
7.2.2 使用自定义注解 @Qualifier
使用 @Named 方式只能硬编码,因为注解的特性不能穿入一个静态的String,很容易写错或后期重构容易遗漏。先根据需要的分类定义注解,并使用 @Qualifier 修饰来告诉 Hilt 这个注解是用来分类的,其它步骤和 @Named 相似。
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OneString
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TwoString
@Module
@InstallIn(ActivityComponent::class)
object StringModule{
@Provides
@OneString
fun providesOneString(): String = "One"
@Provides
@TwoString
fun providesTwoString(): String = "Two"
}
@AndroidEntryPoint
class DemoFragment : Fragment() {
@Inject @OneString lateinit var oneString: String
@Inject @TwoString lateinit var twoString: String
}
八、为Android生成组件
Android类 | 生成的Hilt组件 | 可指定的作用域 | 创建实际~销毁时机 |
Application | ApplicationComponent | @Singleton | Application:onCreate()~onDestroy() |
ViewModel | ActivityRetainedComponent | @ActivityRetainedScope | Activity:onCreate()~onDestroy() |
Service | ServiceComponent | @ServiceScoped | Service:onCreate()~onDestroy() |
Activity | ActivityComponent | @ActivityScoped | Activity:onCreate()~OnDestroy() |
View | ViewComponent | @ViewScoped | View:super()~视图销毁 |
Fragment | FragmentComponent | @FragmentScoped | Fragment:onAttach()~onDestroy() |
@WithFragmentBindings 注解的View | ViewWithFragmentComponent | @ViewScoped | View:super()~视图销毁 |
8.1 组件的生命周期
注入一个Android类时都会有关联对应的 Hilt 组件(component),组件也有相同的生命周期不然会内存泄漏。模块会通过 @InstallIn 装载到组件中,组件便可以为 Android 类提供依赖了(对象的创建、注入、销毁)。
8.2 组件的作用域
默认情况下 Hilt 中所有绑定的依赖项都没有限定作用域,也就是每次代码调用这个字段时都会新建一个实例,当需要共享一个实例时,就需要给依赖项限定作用域(指定的作用域必须跟模块装载到的组件作用域一致,否则报错),即在对应的Android类中为单例。
@Singleton //指定作用域
class Demo @Inject constructor(){
fun getString(): String{
return "Android"
}
}
@Module
@InstallIn(ActivityComponent::class)
object StringModule {
@ActivityScoped //指定作用域
@Provides
fun providesOneString() = "One"
}
8.3 组件的层次结构
当一个依赖项的作用域是整个APP,那在Activity中肯定可以访问到,作用域存在包含关系也就是组件存在层次结构。当模块装载到组件后,模块所绑定的依赖项也可以用于该组件层次结构以下的子组件绑定。