类中使用的某个对象不是在这个类中实例化的(如Activity无法手动实例化使用),而是通过外部注入(从外部传入对象后使用),这种实现方式就称为依赖注入 Dependency Injection(简称DI)。
构造注入 | 将对象B通过构造传参给classA。 | 有些对象无法通过实例化使用,如Activity。 |
字段注入 | 将对象C通过函数设置给classA的字段(也叫setter注入、属性注入)。 | 如果类的依赖类型非常多,而且要严格执行顺序(如造车前要造好轮子,造轮子又需要先造好螺丝和轮胎),随着项目越发复杂需要编写很多模板代码耦合度也更高,手动注入就容易出错。 |
方法注入 | 将对象D传入到classA的方法中,仅在该方法中使用。 | |
工厂注入 | ClassA调用工厂类生产对象 | 调用和生产不在同一个地方,不利于修改测试。 |
单例注入 | ClassA调用单例类获取其持有的对象 | 对象的生命周期难以管理,通常并不需要存在于整个APP生命周期,指定在特定的生命周期又需要添加很多判断。 |
自动注入 |
基于反射的解决方案,可以在运行时连接依赖类型 | 过多使用反射方法会影响程序的运行效率,而且反射方法在编译阶段是不会产生错误的,导致只有在程序运行时才可以验证反射方法是否正确。Square开发的Dagger。 |
静态解决方案(通过注解),可生成在编译时连接依赖类型的代码 | 在编译时就可以发现依赖注入使用的问题。谷歌基于Dagger开发出Dagger2和Hilt,Dagger2使用繁琐,而Hilt专门面向Android开发提供更简单的实现方式,和其它Jetpack组件能更好的协同工作。 |
最新版本
plugins {
id 'com.google.dagger.hilt.android' version "2.44" apply false
}
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 注解,会触发 Hilt 的代码生成。生成的这一 Hilt 组件会附加到 Application 对象的生命周期,并为其提供依赖类型。此外它也是应用的父组件,这意味着其他组件可以访问它提供的依赖类型。
使用 @AndroidEntryPoint 对以下几种 Android 类添加注解后,就可以向它里面的字段注入依赖了。
支持的 Android 类 | 使用的注解 | 说明 |
Activity | @AndroidEntryPoint | 仅支持扩展 ComponentActivity 的 Activity(如AppCompatActivity)。 |
Fragment | 仅支持扩展 androidx.Fragment 的 Fragment,不支持保留的 fragment。 | |
View | ||
Service | ||
BroadcastReceiver | ||
ViewModel | @HiltViewModel |
声明一个延迟初始化(lateinit var)的属性并添加 @Inject 注解。
@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也要进行绑定依赖
被依赖类型的构造函数我们无法添加注解(无构造的接口类型、不属于自己的类型如String、必须使用构建器模式创建实例如Retrofit),就需要通过 @Module 手动创建一个模块类,通过 @InstallIn(XXX::class) 将模块装载到指定的 Hilt 组件中(不同的Android类有对应的Hilt组件),并通过函数提供实例@Binds @Provids,模块便可以为对应的 Android 类提供依赖(对象的创建、注入、销毁)。
注解模块类 | @Module | 告知 Hilt 如何提供该类型的实例。 |
@InstallIn | 告知 Hilt 模块将装载到哪个 Hilt 组件(用于哪个Android 类)。 | |
注解函数 | @Binds | 提供接口实例。必须对抽象函数注解所以类也是抽象的。返回值告知提供哪种接口类型的实例,参数告知该接口的实现类型(该类型也需要对构造注释)。 |
@Provides | 提供实例。可以对class注解,若只包含@provides函数定义为object更高效。返回值告知提供哪种类型的实例,参数告知提供的实例还依赖了哪些类型(这些类型也需要对构造注释),函数体告知如何创建实例(每当需要提供实例时都会执行函数体)。 |
interface IWork
class WorkImpl @Inject constructor(): IWork {}
@Module
@InstallIn(ActivityComponent::class)
abstract class WorkModule {
@Binds
abstract fun bindIWork(workImpl: WorkImpl): IWork //又依赖了WorkImpl类型,即实现类也要进行绑定依赖
}
@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()
}
}
实际开发中可能需要创建同类型的多个不同实现的对象使用,如 Student("张三") 和 Student("李四")、String("A") 和 String(“B”)。上面的方式只能为目标类型提供相同实现的对象,通过使用限定符来实现区分不同实现。
只需要对 @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
}
使用 @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 类会生成一个对应的 Hilt 组件类(component),模块通过 @InstallIn(XXX::class) 装载到指定的组件中,组件便可以为对应的 Android 类提供依赖(对象的创建、注入、销毁)。
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()~已销毁 |
组件与对应的 Android 类有着相同的生命周期,不然会内存泄漏。
默认情况下 Hilt 中所有的绑定都没有限定作用域,也就是每次代码访问字段时都会新建一个实例,当需要共享一个实例时,就需要给绑定限定作用域,即提供的实例在对应的Android类中为单例(同一个Activity中保持单例,不同Activity中实例不同)。
@ActivityScoped //指定作用域
class Demo @Inject constructor() {...}
@Module
@InstallIn(ActivityComponent::class)
object StringModule {
@ActivityScoped //指定作用域
@Provides
fun providesOneString() = "One"
}
当一个依赖类型的作用域是整个APP,那在Activity中肯定可以访问到,作用域存在包含关系也就是组件存在层次结构。当模块装载到组件后,模块所绑定的依赖类型也可以用于该组件层次结构以下的子组件绑定。
每个组件都有默认绑定的依赖类型,因此可以直接使用而不用手动绑定依赖。此外还可以使用 @ApplicationContext 和 @ActivityContext 来获得上下文的绑定。
class Demo @Inject constructor(
val activity: Activity,
@ActivityContext val context: Context
)
给 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()
}
如果 ViewModel 的作用域限定为导航图,使用 hiltNavGraphViewModels( ),该函数可与带有 @AndroidEntryPoint 的Fragment 搭配使用。
implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'
val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)