一、概念
依赖管理不仅可以很好地解决对象繁琐的初始化逻辑,还可以很好的实施控制反转的编码思想。
类中使用的某个对象不是在这个类中实例化的(如汽车类内部创建轮子对象),而是通过外部注入(在外部初始化后传入后使用,汽车不再依赖轮子类实现解耦,可以很容易更换不同类型的轮子,或测试时模拟一个轮子而不用修改汽车类),这种实现方式就称为依赖注入 Dependency Injection(简称DI),也叫控制反转 Inversion of Control(简称 IoC,对象的创建交给外部)。
不只是单纯的传参作用,使得我们的代码更加模块化,每个类都只关注自己的职责,不需要关心其依赖对象的创建和管理,使得代码更容易重用:
- 对于 Activity/Fragment 经常需要访问一些共享资源或服务(如网络请求、数据库访问、ViewModel等),没有 DI 就需要手动创建这些对象导致代码重复和耦合难以单元测试,使用 DI 就可以统一配置然后在需要的地方自动注入。
- 对于 APP/Moudle 不同模块需要访问一些共享的服务,没有 DI 就需要通过复杂的方式获取这些服务导致代码复杂难管理,使用 DI 可以在 APP 中配置后在 Moudle 中自动注入。
- 对于单例(如数据库、网络客户端),使用 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 'kotlin-kapt'
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 对象的生命周期,作为应用的父组件可为其它组件提供依赖类型。
@HiltAndroidApp
class APP : Application() {}
四、配置Android类(@AndroidEntryPoint)
使用 @AndroidEntryPoint 对以下几种 Android 类添加注解后,就可以向它里面的字段注入依赖了。
- 为某个 Android 类添加注解,则必须为依赖于该类的其它 Android 类添加注解(例如为 FragmentA 添加注解则必须为所有使用该 FragmentA 的 Activity 添加注解)。
支持的 Android 类 | 使用的注解 | 说明 |
Activity | @AndroidEntryPoint | 仅支持继承自 ComponentActivity 的 Activity(如AppCompatActivity)。 |
Fragment | 仅支持继承自 androidx.Fragment 的 Fragment,不支持 android.app.Fragment。 | |
View | ||
Service | ||
BroadcastReceiver | ||
ViewModel | @HiltViewModel |
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {}
五、通过 @Inject 注入
对于字段 | Hilt会自动注入相应的依赖。 |
对于构造函数 | Hilt会使用它来创建类的实例。 |
对于函数 | Hilt会在注入后调用它。 |
5.1 配置目标类型的构造函数(绑定信息)
为被依赖类型的构造函数添加 @Inject 注解,向 Hilt 告知如何创建被依赖类型的实例。
- 若构造函数有参数,参数类型也要进行依赖配置。
- private 修饰的构造函数无法注入,使用 @Module 方式。
//无参
class LogBean @Inject constructor() {}
//有参
data class LogBean @Inject constructor(
val userName: String, //又依赖了String类型,String也要进行绑定依赖
val time: TimeBean //又依赖了TimeBean类型,TimeBean也要进行绑定依赖
)
5.2 Android类中引入目标对象(字段注入)
声明一个延迟初始化(lateinit var)的属性并添加 @Inject 注解。
- 注入的字段不能为 private 会导致编译错误。
@AndroidEntryPoint
class LoginFragment : Fragment() {
//lateinit属性未手动初始化,依赖注入提供了实例,所以不会报错
@Inject lateinit var logBean: LogBean //不能为private
}
六、通过 @Module 注入
被依赖类型的构造函数我们无法添加 @Inject 注解时(private修饰的构造、接口类型没有构造、不属于自己的类型如String、必须使用构建器模式创建实例如Retrofit),就需要通过 @Module 手动创建一个模块类,通过 @InstallIn(XXX::class) 将模块装载到指定的 Hilt 组件中(不同的Android类有对应的Hilt组件),并通过函数提供实例 @Binds @Provids,模块便可以为对应的 Android 类提供依赖(对象的创建、注入、销毁)。
注解类 | @Module | 告知 Hilt 如何提供该类型的实例。 |
@InstallIn | 告知 Hilt 模块将装载到哪个 Hilt 组件(用于哪个Android 类)。 | |
注解函数 | @Binds | 提供接口实例。必须对抽象函数注解所以类也是抽象的。返回值告知提供哪种接口类型的实例,参数告知该接口的具体实现类型(该类型也需要对构造注释)。 |
@Provides | 提供实例。可以对class注解,若只包含@provides函数定义为object更高效。返回值告知提供哪种类型的实例,参数告知提供的实例还依赖了哪些类型(这些类型也需要对构造注释),函数体告知如何创建实例(每当需要提供实例时都会执行函数体)。 | |
@Singleton | 用于@Provides注解的方法或@Inject注解的构造函数,告诉Hilt提供的依赖是单例的。 | |
@ViewModelInject | 用于ViewModel的构造函数,告诉Hilt如何创建ViewModel的实例。 |
6.1 提供单个实例
6.1.1 提供抽象类/接口实例 @Binds
- 函数参数的类型,即接口的实现类也要进行绑定依赖。
interface IWork {
fun show()
}
class WorkImpl @Inject constructor(): IWork {
override fun show() {}
}
@Module
@InstallIn(ActivityComponent::class)
abstract class WorkModule {
@Binds
abstract fun bindIWork(workImpl: WorkImpl): IWork //又依赖了WorkImpl类型,即实现类也要进行绑定构造
}
6.1.2 提供单个实例 @Provides
- 若函数有参数,参数类型也要进行绑定依赖。
@Module
@InstallIn(ActivityComponent::class)
object RetrofitModule {
@Provides
fun provideRetrofit(okHeepClient: OkHttpClient): Retrofit { //又依赖了OkHttpClient类型,OkhttpClient也要进行绑定依赖
return Retrofit.Builder()
.client(okHeepClient)
.baseUrl(ApiService.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
6.2 提供同类型不同实现的多个实例
实际开发中可能需要创建同类型的多个不同实现的对象使用,如 Student("张三") 和 Student("李四")、String("A") 和 String(“B”)。上面的方式只能为目标类型提供相同实现的对象,通过使用限定符来实现区分不同实现。
6.2.1 使用 @Named
只需要对 @Binds 或 @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
}
6.2.2 使用自定义注解 @Qualifier
使用 @Named 方式只能硬编码,因为注解的特性不能穿入一个静态的String,很容易写错或后期重构容易遗漏。先根据需要的分类定义注解,使用 @Qualifier 声明作用是为相同类型注入不同实例,使用 @Retention 声明注解的作用范围(AnnotationRetention.BINARY表示注解在编辑后将会被保留),用法和 @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 组件类(component),模块通过 @InstallIn(XXX::class) 装载到指定的组件中,组件便可以为对应的 Android 类提供依赖(对象的创建、注入、销毁)。
- 不会为 Broadcast 生成组件,会直接从 SingletonComponent 注入广播接收器。
- ActivityRetainedComponent(推荐使用)不会随着Activity旋转销毁而销毁,而ActivityComponentd每次都会重新生成组件。
Android类 | 生成的Hilt组件类 | 可指定的作用域 | 默认绑定的依赖类型 | 创建实际~销毁时机 |
Application | SingletonComponent | @Singleton | Application | Application:onCreate()~已销毁 |
不适用 | ActivityRetainedComponent | @ActivityRetainedScope | Application | Activity:onCreate()~onDestroy() |
ViewModel | ViewModelComponent | @ViewModelScope | SavedStateHandle | ViewModel:已创建~已销毁 |
Service | ServiceComponent | @ServiceScoped | Application、Service | Service:onCreate()~onDestroy() |
Activity | ActivityComponent | @ActivityScoped | Application、Activity | Activity:onCreate()~OnDestroy() |
View | ViewComponent | @ViewScoped | Application、Activity、View | View:super()~已销毁 |
Fragment | FragmentComponent | @FragmentScoped | Application、Activity、Fragment | Fragment:onAttach()~onDestroy() |
@WithFragmentBindings 注解的View | ViewWithFragmentComponent | @ViewS coped | Application、Activity、Fragment、View | View:super()~已销毁 |
7.1 组件的生命周期
组件与对应的 Android 类有着相同的生命周期,不然会内存泄漏。
- ActivityRetainedComponent 在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁。
7.2 组件的作用域
默认情况下 Hilt 中所有的绑定都没有限定作用域,也就是每次代码访问字段时都会新建一个实例,当需要共享一个实例时,就需要给绑定限定作用域,即提供的实例在对应的Android类中为单例(同一个Activity中保持单例,不同Activity中实例不同)。
- 指定的作用域必须与其装载到的组件的作用域一致,否则报错。
@ActivityScoped //指定作用域
class Demo @Inject constructor() {...}
@Module
@InstallIn(ActivityComponent::class)
object StringModule {
@ActivityScoped //指定作用域
@Provides
fun providesOneString() = "One"
}
7.3 组件的层次结构
当一个依赖类型的作用域是整个APP,那在Activity中肯定可以访问到,作用域存在包含关系也就是组件存在层次结构。当模块装载到组件后,模块所绑定的依赖类型也可以用于该组件层次结构以下的子组件绑定。
- ViewComponent 可以使用 ActivityComponent 中绑定的依赖类型,如果还需要使用 FragmentComponent 中的绑定并且视图是 Fragment 的一部分,用该将 @WithFragmentBindings 注解和 @AndroidEntryPoint 一起使用。
7.4 组件默认绑定的依赖类型
每个组件都有默认绑定的依赖类型,因此可以直接使用而不用手动绑定依赖。此外还可以使用 @ApplicationContext 和 @ActivityContext 来获得上下文的绑定。
class Demo @Inject constructor(
val activity: Activity,
@ActivityContext val context: Context
)
八、ViewModel 中使用
8.1 ViewModel
给 ViewModel 添加 @HiltViewModel,并对构造函数使用 @Inject。在带有 @AndroidEntryPoint 的 Activity/Fragment 中可以使用 ViewModelProvider 或 by viewModels() 来获取实例。
实例由 ViewModelComponent 提供,它和 ViewModel 有相同的生命周期,因此可在配置更改后继续存在。如果需要每次访问获取的实例是同一个,使用 @ViewModelScope 限制作用域。
@HiltViewModel
class DemoViewModel @Inject constructor(
private val avedStateHandle: SavedStateHandle,
private val repository: DemoRepository
) : ViewModel() {}
@AndroidEntryPoint
class DemoActivity : AppCompatActivity() {
private val viewModel: DemoViewModel by viewModels()
}
8.2 Navigation
如果 ViewModel 的作用域限定为导航图,使用 hiltNavGraphViewModels( ),该函数可与带有 @AndroidEntryPoint 的Fragment 搭配使用。
implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'
val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)
九、实际使用场景举例
9.1 SharedPreference注入
// 创建一个Hilt模块,用于提供SharedPreferences实例
@Module
@InstallIn(SingletonComponent::class)
object SharedPreferencesModule {
@Provides
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("pref_name", Context.MODE_PRIVATE)
}
}
// 在Activity中,你可以使用@Inject注解来请求一个SharedPreferences实例。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var sharedPreferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 使用SharedPreferences
val editor = sharedPreferences.edit()
editor.putString("key", "value")
editor.apply()
}
}
9.2 多模块项目
假设有一个data模块和一个app模块,data模块提供了一个Repository类,app模块需要使用这个Repository。
//首先,在data模块中,你定义了一个Repository类,并用@Inject注解标记其构造函数:
class Repository @Inject constructor() {}
//然后,在app模块中,你可以直接在需要的地方注入Repository:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: Repository
}
9.3 Retrofit
NetworkModule是一个Hilt模块,它在应用级别的组件(SingletonComponent)中提供了一个Retrofit实例。有一个Repository类,它需要这个Retrofit实例来发起网络请求。有一个ViewModel
,它需要这个Repository
来获取数据。
这就是一个典型的依赖链:MyViewModel依赖于Repository,Repository依赖于Retrofit。通过Hilt,我们可以轻松地管理这个依赖链,而无需手动创建和管理每个依赖。这使得代码更加清晰和直观,也使得新成员更容易理解项目的结构。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
}
}
class Repository @Inject constructor(private val retrofit: Retrofit) {}
class MyViewModel @ViewModelInject constructor(
private val repository: Repository
) : ViewModel() {}
9.4 数据库
// 创建一个Hilt模块,用于提供Room数据库实例
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
fun provideDatabase(@ApplicationContext context: Context): MyDatabase {
return Room.databaseBuilder(
context,
MyDatabase::class.java, "database-name"
).build()
}
@Provides
fun provideUserDao(database: MyDatabase): UserDao {
return database.userDao()
}
}
// 在需要UserDao的地方,使用@Inject注解来请求一个UserDao实例。
class UserRepository @Inject constructor(private val userDao: UserDao) {
// ...
}