Hilt依赖注入
依赖
- 在项目级
build.gradle中,添加
buildscript {
...
ext.hilt_version = '2.46.1'
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
- 在app级
build.gradle中,添加
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
可以参考 官方链接 配置hilt最新版本
使用
为什么使用依赖注入?
官方开发人员的博客原文:
By following DI principles, you lay the groundwork for good app architecture, greater code reusability, and ease of testing.
通过遵循依赖注入原则,您可以为良好的app架构,更好的代码复用性以及更好的可测试性奠定基础。
Android 常用的依赖注入框架有 Dagger2 以及 Hilt。Hilt是专门面向 Android 的依赖注入框架,它更简单。而且 Dagger2 可以和 Hilt 共存。
如何使用?
首先,使用 @HiltAndroidApp 注解应用的 Application。所有的 Hilt 应用都必须包含带有此注解的 Application 类,该注解会触发 Hilt 的代码生成操作。生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。
@HiltAndroidApp
class MyApplication : Application() {
...
}
目前 Hilt 支持以下 Android 类:
- Application(通过使用
@HiltAndroidApp) - ViewModel(通过使用
@HiltViewModel) - Activity
- Fragment
- View
- Service
- BroadcastReceiver
除了特殊标注的两个类外,其他类使用 @AndroidEntryPoint 注解。
@AndroidEntryPoint
class MyActivity : AppCompatActivity() { ... }
如果使用 @AndroidEntryPoint 为某个 Android 类添加注解,则还必须为依赖于该类的 Android 类添加注解。例如,如果为某个 fragment 添加注解,则还必须为使用该 fragment 的所有 activity 添加注解。
@AndroidEntryPoint 会创建一个沿袭 Android 类生命周期的依赖项容器,即组件(Component)。Hilt 一共内置了8种组件类型,这些组件可以从它们各自的父类接受依赖项。
组件(Component) 和 作用域(Scope) 共同决定了依赖项的生命周期和可见性,详细的内容后文再谈。
字段注入
以向 Activity 中注入一个 Adapter 为例。
首先,使用 @Inject 注解 adapter 的构造方法来提供 MyAdapter 的实例:
class MyAdapter @Inject constructor() { ... }
然后,在已经使用 @AndroidEntryPoint 的 Activity 中,使用 @Inject 注解执行字段注入
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var myAdapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// myAdapter 在这里就已经是被注入,可用的状态了
}
}
再来看另一种常见的场景,对 ViewModel 的支持:
当我们的项目中添加了 androidx.activity:activity-ktx:$Version 依赖项的话,我们可以使用委托来构造 ViewModel
private val viewModel: MyViewModel by viewModels()
如果 ViewModel 的构造方法中包含参数,hilt 可以优化掉使用的工厂类。ViewModel 如下所示
@HiltViewModel
class MyViewModel @Inject constructor(
private val myAdapter: MyAdapter
): ViewModel() { ... }
Activity 中的使用:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
val viewModel: MyViewModel by viewModels()
}
hilt 还支持其他的 Jetpack库,可以参阅此 官方文档
模块
Hilt 模块是一个带有 @Module 和 @InstallIn 注解的类,@InstallIn 注解通过指定 Hilt 组件告知 Hilt 绑定在哪些容器中可用。Hilt 模块用于向 Hilt 添加绑定,即告知 Hilt 如何提供不同类型的实例。
Hilt 模块主要用于不能通过构造方法注入的类型添加绑定,例如接口和外部库的类。
接口注入 @Binds
使用 @Binds 注入接口实例。
假设我们有这样的一个接口:
interface MyInterface {
fun log()
}
则将实现类添加 @Inject 注解以告知 Hilt 如何提供其实例。
class MyInterfaceImpl @Inject constructor(
...
) : MyInterface {
...
}
然后,需要新建一个抽象类,类名没有强制要求,因为我们不会调用这个类,一般按照习惯叫 xxModule,具体名称和业务相关,体现提供信息类型即可。除此以外,按前文所述,需要配套使用 @Module 和 @InstallIn 两个注解。
因为我们不需要事先具体方法,所以这里定义的是抽象方法。通过对模块内的抽象方法使用 @Binds 注解以告知 Hilt 为接口使用哪种实现。抽象方法的名称也是随意的。
抽象方法的返回值类型是我们要提供实现的接口(如 MyInterface),方法的唯一参数类型必须是该接口的实现类。
@Module
@InstallIn(ActivityComponent::class)
abstract class MyModule {
@Binds
abstract fun bindImpl(impl: MyInterfaceImpl): MyInterface
}
现在,Hilt 具有了注入 MyInterfaceImpl 实例的所有信息。我们已经可以在 Activity 中使用了。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit val impl: MyInterface
fun testFun() {
impl.log()
}
}
实例注入 @Provides
接口并不是唯一一种无法通过构造方法注入类型的情况。如果某个类不归我们所有,比如 Retrofit、OkHttpClient 或 Room 数据库 等类。或者 建造者模式 创建的实例。
Hilt 使用 @Provides 注解告知 Hilt 如何提供无法通过构造方法注入的类型的实例。
带有此注解的方法会告知 Hilt 如下信息:
- 方法返回类型会告知 Hilt 方法提供哪个类型的实例。
- 方法参数会告知 Hilt 相应类型的依赖项。
- 方法主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行方法主体。
这里以 官方样例 中的代码为例
@Module
@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()
}
}
provideLogDao 方法告知 Hilt 提供 LogDao 的实例时需要执行 datbase.logDao()。
默认情况下,Hilt 中的所有绑定都未限定作用域,Hilt 这里的默认行为意味着每当请求绑定时,Hilt 会创建一个新实例。而由于我们希望 Hilt 始终提供相同的数据库实例,所以为 provideDatabase 方法添加了 @Singleton 注解。关于作用域的详细信息后文再介绍。
在 Kotlin 中,仅包含 @Provides 函数的模块可以是 object 类。通过这种方式,提供程序会得到优化,并几乎内嵌到生成的代码中。
Hilt 模块不能同时包含非静态和抽象绑定方法,因此 @Binds 和 @Provides 注解不能放在同一个类中。
限定符
限定符是用于标识绑定的注解,用于告知 HIlt 给相同类型注入不同实例(多个绑定)
假如我们的接口有两种实现,按前文所述内容,实现类应分别如下。
class ImplA @Inject constructor(): MyInterface {
override fun log() {
Log.d("ImplA", "实现A")
}
}
class ImplB @Inject constructor(): MyInterface {
override fun log() {
Log.d("ImplB", "实现B")
}
}
为了告知 Hilt 我们使用哪一种实现,我们需要定义为 @Binds 或 @Provides 方法添加注解的限定符,我们需要为每种实现定义一个限定符。
// MyModule.kt
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindImplA
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindImplB
@Retention,是用于声明注解的作用范围,选择 AnnotationRetention.BINARY 表示该注解在编译之后会得到保留,但是无法通过反射去访问这个注解。一般都是这个。
现在我们为在 Module 中的提供实现的 @Binds 方法添加注解。
@Module
@InstallIn(ActivityComponent::class)
abstract class MyModule {
@Binds
@BindImplA
abstract fun bindImplA(impl: ImplA): MyInterface
@Binds
@BindImplB
abstract fun bindImplB(impl: ImplB): MyInterface
}
最后,限定符也要用于要注入的实现。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@BindImplA
@Inject lateinit val impl: MyInterface
}
预定义限定符
预定于限定符主要处理需要 Activity 和 Application 的 Context 类的情况。
可以使用 @ApplicationContext 获得应用上下文绑定,可以使用 @ActivityContext 获得 activity 上下文绑定。
例如:
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
组件
前文我们提到过:
@AndroidEntryPoint会创建一个沿袭 Android 类生命周期的依赖项容器,即组件(Component)。
对于可以执行字段注入的每个 Android 类,都有一个相关联的 Hilt 组件。我们使用 @InstallIn 注解中引用的也是这些组件。每个 Hilt 组件负责将其绑定注入相应的 Android 类。
组件生命周期及层次结构
Hilt 提供的组件如下表所示:
| Hilt 组件 | 注入器面向的对象 | 创建时机 | 销毁时机 |
|---|---|---|---|
SingletonComponent | Application | Application#onCreate() | Application 已销毁 |
ActivityRetainedComponent | 不适用 | Activity#onCreate() | Activity#onDestroy() |
ViewModelComponent | ViewModel | ViewModel 已创建 | ViewModel 已销毁 |
ActivityComponent | Activity | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | Fragment | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | View | View#super() | View 已销毁 |
ViewWithFragmentComponent | 带有 @WithFragmentBindings 注解的 View | View#super() | View 已销毁 |
ServiceComponent | Service | Service#onCreate() | Service#onDestroy() |
ActivityRetainedComponent 在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁。
将模块安装到组件后,其绑定就可以用作该组件中其他绑定的依赖项,也可以用作组件层次结构中该组件下的任何子组件中其他绑定的依赖项,即依赖是由高层次组件向低层次组件传递的,如果绑定在层次结构的较高层级中可用,那么在较低层级的容器中也可用。
组件层次结构如下图所示:
按我们上文的例子,如果我们想要 MyModule 中提供的接口实现实例能在 Service 中进行依赖注入,我们可以修改注解为 @InstallIn(SingletonComponent::class)。
组件作用域
前文我们提到过:
组件(Component) 和 作用域(Scope) 共同决定了依赖项的生命周期和可见性。
参照上一节中官方的层次结构图,我们可知组件与作用域的关系如下表:
| Android 类 | 生成的组件 | 作用域 |
|---|---|---|
Application | SingletonComponent | @Singleton |
Activity | ActivityRetainedComponent | @ActivityRetainedScoped |
ViewModel | ViewModelComponent | @ViewModelScoped |
Activity | ActivityComponent | @ActivityScoped |
Fragment | FragmentComponent | @FragmentScoped |
View | ViewComponent | @ViewScoped |
带有 @WithFragmentBindings 注解的 View | ViewWithFragmentComponent | @ViewScoped |
Service | ServiceComponent | @ServiceScoped |
Hilt 只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。即将对象A范围限定到另一个对象B意味着在B的整个生命周期中,它将始终具有相同的A实例。
前文我们提到过:
默认情况下,Hilt 中的所有绑定都未限定作用域,Hilt 这里的默认行为意味着每当请求绑定时,Hilt 会创建一个新实例。
现在我们可以更好的理解这句话。
我们把模块安装进了 ActivityComponent 并不意味着我们内部提供实例的方法拥有了默认的 @ActivityScoped 作用域注解。此时这种未限定作用域的默认行为会导致我们每次请求绑定时,Hilt 会提供一个新的实例。
如果像是 retrofit 实例或官方 Demo 中的 Room 相关的实例这样,我们全局只需要一份,不需要重复创建的实例,我们可以借助 @Singleton 注解来更改这种默认行为。当然此时模块的 @InstallIn 注解也需要更改为 SingletonComponent。
官方的默认行为应该是出于性能方面的考量。当我们限定了作用域的情况下,提供的对象在容器被销毁之前会一直保留在内存中。
关于这里的详细信息,可以参阅此 官方博客 。
组件默认绑定
每个 Hilt 组件都附带一组默认绑定,Hilt 可以将其作为依赖项注入您自己的自定义绑定。
| Android 组件 | 默认绑定 |
|---|---|
SingletonComponent | Application |
ActivityRetainedComponent | Application |
ViewModelComponent | SavedStateHandle |
ActivityComponent | Application、Activity |
FragmentComponent | Application、Activity 和 Fragment |
ViewComponent | Application、Activity 和 View |
ViewWithFragmentComponent | Application、Activity、Fragment、View |
ServiceComponent | Application、Service |
官方提到这些绑定对应于常规 activity 和 fragment 类型,而不对应于任何特定子类。这里可以优化掉预定义限定符。例如官方代码如下:
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext context: Context
) : AnalyticsService { ... }
// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
application: Application
) : AnalyticsService { ... }
class AnalyticsAdapter @Inject constructor(
@ActivityContext context: Context
) { ... }
// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
activity: FragmentActivity
) { ... }
其中我们可以看到官方的代码中使用了 FragmentActivity 这个 activity 子类。我们可以通过 IDE 左面的红色向上箭头找到它的来源,我们也可以通过两叉向下的图标找到这个实现提供给了谁。
这个操作能通过编译得益于官方封装的 ActivityModule。
// ActivityModule.class
@Module
@InstallIn({ActivityComponent.class})
abstract class ActivityModule {
@Binds
@ActivityContext
abstract Context provideContext(Activity activity);
@Provides
@Reusable
static FragmentActivity provideFragmentActivity(Activity activity) {
try {
return (FragmentActivity)activity;
} catch (ClassCastException var2) {
throw new IllegalStateException("Expected activity to be a FragmentActivity: " + activity, var2);
}
}
private ActivityModule() {
}
}
我们可以看到原理其实就是强制转换。我们可以通过同样的思路,为全局只会存在一份的 Application 实例创建一个模块来提供我们项目内自定义封装的 Application 类。
@InstallIn(SingletonComponent::class)
@Module
class AppModule {
@Provides
fun bindImplA(application: Application): LogApplication {
return application as LogApplication
}
}
细心的同学可能还记得我们上文说到过:
Hilt 模块不能同时包含非静态和抽象绑定方法,因此 @Binds 和 @Provides 注解不能放在同一个类中。
而官方的这个模块似乎违反了这个规定。这里我还不是很确定这里的原因,但是我们自己写是不能违反这个限制的,否则我们会得到以下报错:
A @Module may not contain both non-static and abstract binding methods
ViewModel 的注入
由于前文提到过的 官方博客 以及 郭霖老师的博客 中,ViewModel 的注入 @ViewModelInject 已经被弃用,所以补充一下当前版本下的 ViewModel 注入的方法。
当前版本在 ViewModel 处使用 @HiltViewModel 注解,在 ViewModel 的构造方法中使用 @Inject 注解。然后在带有 @AndroidEntryPoint 注解的 activity 或 fragment 可以使用 ViewModelProvider 或 by viewModels() KTX 扩展 照常获取 ViewModel 实例:
// viewmodel.kt
@HiltViewModel
class TasksViewModel @Inject constructor(
val taskRepository: TaskRepository
) : ViewModel() {
}
// activity.kt
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
private val exampleViewModel: TasksViewModel by viewModels()
...
}
此处内容可以查阅 官方文档。此文档还记述了其他 Jetpack 库的集成。
不支持的入口点
个人感觉此处功能并不常用,所以摸了。
有想了解这块的同学可以查阅 官方文档。
参考资料及推荐文章
以下是学习 Hilt 并记录此整理时看过的官方文档或其他博客。