[TOC]
1 Hilt相较于Dagger的优势
在我们了解Hilt之前,先需要知道Dagger, Dagger是Google提供的用于依赖注入的库,该库很多人可能都听过.Dagger的特点总结一下就是:牛逼,高端,难用.那这么牛逼高端的库为什么不用?答案就是后边两个字:难用!!!至于为什么难用,主要原因是该库是为了Java,Kotlin以及Android设计的,是为了解决基于反射解决方案引起的许多开发和性能问题,详细解释可以看看Dagger官网[https://dagger.dev/]以及我之前写的一篇简单文章[Dagger简单使用及实现原理(https://www.jianshu.com/p/9d80a6cb59f2])]
因为太难用了,所以Google专门为我们提供了一个使用更简单的依赖注入库Hilt.
Hilt是Android的依赖注入库,可减少在项目中执行手动DI[依赖注入]的样板代码,执行手动依赖注入需要手动构造每个类及依赖项,通过组件管理依赖项.
2 添加依赖项
首先将hilt-android-gradle-plugin
插件添加到根项目的build.gradle
文件中:
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
然后在app/build.gradle文件中添加依赖项:
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
这里有一个需要注意的地方:同时使用 Hilt 和数据绑定的项目需要 Android Studio 4.0 或更高版本,最好是>=4.1版本,新版本支持依赖注入指向。
Hilt使用Java8
功能,如需在项目中启动Java8,请在app/build.gradle文件中增加如下代码:
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
3 Hilt增加应用类注解
所有使用Hilt的项目都需要包含一个带有@HiltAndroidApp
注释的Application
类.
@HiltAndroidApp
会触发Hilt代码生成,生成的代码包含应用的一个基类,该基类充当应用级依赖项容器。
3_1 Hilt为何要增加@HiltAndroidApp
注解
首先我们进入@HiltAndroidApp`看看Google的注释
package dagger.hilt.android;
...
/*
*
* {@literal @}HiltAndroidApp(Application.class)
* public final class FooApplication extends Hilt_FooApplication {
* {@literal @}Inject Foo foo;
*
* {@literal @}Override
* public void onCreate() {
* super.onCreate(); // The foo field is injected in super.onCreate()
* }
* }
*
*
* @see AndroidEntryPoint
*/
// Set the retention to RUNTIME because we check it via reflection in the HiltAndroidRule.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@GeneratesRootInput
public @interface HiltAndroidApp {
}
可以看到Hilt给我们的FooApplication生成了一个Hilt_FooApplication
类,这个Hilt_FooApplication
是做什么的我们稍后看看,另外我们可以看到在onCreate()
我们可以增加依赖注入
我们在看看Hilt_FooApplication
做了什么
public abstract class Hilt_TestHiltApplication extends Application implements GeneratedComponentManager
如上代码是Hilt自动生成的继承自Application的类,该类大致做了两部分操作
-
1 将Hilt自行生成的
ApplicationContextModule
模块添加至DaggerTestHiltApplication_HiltComponents_ApplicationC
组件中。目的是给我们提供需要的applicationContext
,[ApplicationContextModule
]具体源码如下:@Module @InstallIn(ApplicationComponent.class) public final class ApplicationContextModule { private final Context applicationContext; public ApplicationContextModule(Context applicationContext) { this.applicationContext = applicationContext; } @Provides @ApplicationContext Context provideContext() { return applicationContext; } @Provides Application provideApplication() { return (Application) applicationContext.getApplicationContext(); } }
-
2 将
TestHiltApplication
通过injectTestHiltApplication
注入到ApplicationComponent
组件中,具体源码如下:@OriginatingElement( topLevelClass = TestHiltApplication.class ) @GeneratedEntryPoint @InstallIn(ApplicationComponent.class) @Generated("dagger.hilt.android.processor.internal.androidentrypoint.InjectorEntryPointGenerator") public interface TestHiltApplication_GeneratedInjector { void injectTestHiltApplication(TestHiltApplication testHiltApplication); }
总结一下:Application
为什么要增加注解@HiltAndroidApp
?
- 1:将
ApplicationContextModule
添加至应用组件中,获取应用组件applicationContext
- 2:自动实现了依赖注入,免去了类似Dagger的手动调用
4 依赖项注入Android类
在经历了步骤3操作且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint
注释的其他 Android 类提供依赖项:
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }
Hilt 目前支持以下 Android 类:
-
Application
(其中通过使用@HiltAndroidApp
) Activity
Fragment
View
Service
BroadcastReceiver
有两点需要注意:
- Hilt 仅支持扩展
ComponentActivity
的 Activity,如AppCompatActivity
。- Hilt 仅支持扩展
androidx.Fragment
的 Fragment。
我们看看通过@AndroidEntryPoint
注解了Activity之后Hilt帮我们做了什么?
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ActivityGenerator")
public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManager
可以看出来:Hilt自动帮我们生成了一个继承自AppCompatActivity
的名称为Hilt_MainActivity.java类,这个类里边代码不是很多,我们大概总结一下这段代码帮我们做了那些事?
- 1:在onCreate方法中通过
injectMainActivity
将我们的MainActivity注入到了ActivityComponent
组件中 - 2:提供了一个
ViewModelProvider.Factory
工厂类。至于为什么要提供工厂类,其实跟@ViewModelInject
有关,稍后在@ViewModelInject
解释
上述代码有一个注意的地方,代码如下:
private volatile ActivityComponentManager componentManager; ... protected final ActivityComponentManager componentManager() { if (componentManager == null) { synchronized (componentManagerLock) { if (componentManager == null) { componentManager = createComponentManager(); } } } return componentManager; }
上述代码其实是一个双重检查锁,用于检测对象是否初始化,不清楚Java双重检查锁的可以参考Java中的双重检查锁(double checked locking)
ViewModelProvider.Factory
5 增加“@Inject”注入
为了执行字段注入,在Dagger中我们通过@Inject实现依赖项绑定,Hilt中也是同理。
Hilt提供绑定信息的一种方式是构造函数注入。实现如下:
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
带有注释的构造函数的参数即是该类的依赖项。
在上述代码中,AnalyticsService
是AnalyticsAdapter
的一个依赖项。因此,Hilt还必须提供AnalyticsService
的实例。说白了就是:我们现在只是通过@Inject实现了依赖项的注入,但是我们并没有提供AnalyticsService
实例,所以我们还需要定义Module
,通过@Provides
将我们的依赖项提供出去,要不然Hilt会无情的报错。如何提供请看【6_2 @Provides注入实例】
5_1 使用@ViewModelInject注入
与@Inject
一样,Hilt给我们提供了带有注释@ViewModelInect
构造函数的依赖项,用于View用于注入的构造函数。
该ViewModel用于创建由androidx.hilt.lifecycle.HiltViewModelFactory
并且可以在默认情况下检索Activity或Fragment带注解@AndroidEntryPoint
,如下代码:
public class DonutViewModel {
@ViewModelInject
public DonutViewModel(@Assisted SavedStateHandle handle, RecipeRepository repository) {
// ...
}
}
@AndroidEntryPoint
class StatisticsFragment : Fragment() {
private val viewModel by viewModels()
...
}
如上两处代码我们就实现了将View中的依赖项绑定到了StatisticsFragment中了。
之前我们在【4 依赖项注入Android类】有一个问题,为什么Hilt提供的Activity或Fragment为什么要提供ViewModelProvider.Factory
这个工厂类?我们这就看看
我们翻看Hilt提供的Activity或Fragment部分源码可以看到如下:
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ActivityGenerator")
public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManager
可以看到,Hilt_MainActivity中的getDefaultViewModelProviderFactory()
,其实是重写了Activity源码中的ViewModelProvider.Factory getDefaultViewModelProviderFactory()
,目的是为了给我们提供一个DefaultViewModelProviderFactory
,而DefaultViewModelProviderFactory
恰好是我们viewModels
绑定需要的,FragmentViewModelLazy.kt部分源码如下:
@MainThread
inline fun Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
...
@MainThread
fun Fragment.createViewModelLazy(
viewModelClass: KClass,
storeProducer: () -> ViewModelStore,
factoryProducer: (() -> Factory)? = null
): Lazy {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}
到这里我们就彻底明白了Hilt为我们生成的Hilt_XXXActivity
或Hilt_XXXFragment
中的全部代码了!
抛出一个问题,我们想在加注入的构造方法中增加默认值,该怎么做?,答案就是5_1以及5_2
5_2 使用构造方法增加默认参数
从之前所学可知,Hilt提供绑定信息的一种方式是构造函数注入。实现如下:
class AnalyticsAdapter @Inject constructor(
private val title: String,
private val des: String
) { ... }
我们现在想给title
和desc
增加默认值该怎么办?我们在上述代码基础上修改一下,代码如下:
data class AnalyticsAdapter constructor(var title: String, val des: String) {
@Inject
constructor() : this("标题", "描述")
}
至于为什么不直接在构造函数中增加默认值,大概的原因还不支持在绑定的参数上直接赋值的操作,如果实在想支持可以使用5_3 使用预定义限定符操作增加构造参数方式来实现,我们的做法就是如上所示,再创建一个空的构造函数,在调用时Hilt会先调用我们内部的空构造函数,然后调用外部的构造并将值赋给参数。
5_3 限定符操作,为同一类型提供多个绑定
在5_2 使用构造方法增加默认参数当我们需要给定默认值时,我们需要重写构造,是不是比较麻烦,那有没有更简单的方式呢?答案是必须有,我们可以使用@Named或@Qualifier
实现多绑定,继续往下看:
5_3_1 使用@Named实现多个绑定
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Named(NAME_TITLE)
@Provides
fun testNamed(): String {
return "原始Named限定符标题"
}
@Named(NAME_DESC)
@Provides
fun test1Named(): String {
return "原始Named限定符描述"
}
}
const val NAME_TITLE = "NameTitle"
const val NAME_DESC = "NameDesc"
// 第一种 Name方式
data class FakeService @Inject constructor(@Named(NAME_TITLE) var title: String, @Named(NAME_DESC) val des: String)
如上代码:我们首先有一个module提供类,我们通过@Providers
提供了两个testNamed()
和test1Named()
, 是我们需要注入的地方我们通过@Inject constructor(@Named(NAME_TITLE) var title: String, @Named(NAME_DESC) val des: String)
获取我们提供的默认值。
同样的道理,我们用@Qualifier
实现
5_3_2 使用@Qualifier实现多个绑定
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class QualifierTitle
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class QualifierDesc
@QualifierTitle
@Provides
fun testQualifier(): String {
return "原始Qualifier标题"
}
@QualifierDesc
@Provides
fun test1Qualifier(): String {
return "原始Qualifier描述"
}
}
data class Fake1Service @Inject constructor(@AnalyticsModule.QualifierTitle var title: String, @AnalyticsModule.QualifierDesc var des: String)
1:首先我们通过@Qualifier
提供了两个注解类QualifierTitle
和QualifierDesc
2:我们通过@QualifierTitle
和@QualifierDesc
对提供的方法进行限定
3:在注入的地方进行限定操作‘@Inject constructor(@AnalyticsModule.QualifierTitle var title: String, @AnalyticsModule.QualifierDesc var des: String)’
6 创建Hilt模块Module
6_1 @Bind注入接口实例
@Binds
注释会告知Hilt在需要提供接口的实例时要使用哪种实现。
带有注释的函数会向Hilt提供以下信息:
- 函数返回类型会告知Hilt函数提供哪个接口实例。
- 函数参数会告知Hilt提供哪种实现。
我们用官方例子了解一下
//第一步:提供了一个接口`AnalyticsService`
interface AnalyticsService {
fun analyticsMethods()
}
//第二步:`AnalyticsServiceImpl`实现`AnalyticsService`接口,并实现内部方法
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService { ... }
//第三步:可能还存在一个`AnalyticsServiceImpl_Test`也实现`AnalyticsService`接口,并实现内部方法
class AnalyticsServiceImpl_Test @Inject constructor(
...
) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
//第三步:通过`@Binds`将告知`AnalyticsService`的实现为`AnalyticsServiceImpl`而不是‘AnalyticsServiceImpl_Test’
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
6_2 @Provides注入实例
使用无法通过构造函数注入的情况,例如:Retrofit、OkHttpClient
,使用构建器模式创建实例,无法通过构造函数注入。那这个时候我们就可以用@Provides
方式来提供依赖项。
带有注释的函数会向 Hilt 提供以下信息:
- 函数返回类型会告知 Hilt 函数提供哪个类型的实例。
- 函数参数会告知 Hilt 相应类型的依赖项。
- 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体
我们用一段代码来理解一下:
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.build()
}
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
如上代码都不是通过构造函数实现注入。我们可以通过@Provides
来实现对外部提供依赖项
7 Hilt中的预定义限定符
7_1 Google提供的预定义限定符
Android 组件 | 默认绑定 |
---|---|
@ApplicationContext |
获取应用上下文绑定 |
@ActivityContext |
获取Activity上下文绑定 |
7_2 组件提供的限定符
Android 组件 | 默认绑定 | 注入器面向的对象 |
---|---|---|
@ApplicationComponent |
Application |
Application |
@ActivityRetainedComponent |
Application |
ViewModel |
@ActivityComponent |
Application 和 Activity |
Activity |
@FragmentComponent |
Application、Activity 和 Fragment |
Fragment |
@ViewComponent |
Application、Activity 和 View |
View |
@ViewWithFragmentComponent |
Application、Activity、Fragment 和 View |
带有 @WithFragmentBindings 注释的 View |
@ServiceComponent |
Application 和 Service |
Service |
注意:ActivityRetainedComponent` 在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁。如下图:
[图片上传失败...(image-4018ab-1603855174131)]
8 Hilt提供的组件生命周期
8_1 组件生命周期
Hilt 会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。
生成的组件 | 创建时机 | 销毁时机 |
---|---|---|
ApplicationComponent |
Application#onCreate() |
Application#onDestroy() |
ActivityRetainedComponent |
Activity#onCreate() |
Activity#onDestroy() |
ActivityComponent |
Activity#onCreate() |
Activity#onDestroy() |
FragmentComponent |
Fragment#onAttach() |
Fragment#onDestroy() |
ViewComponent |
View#super() |
视图销毁时 |
ViewWithFragmentComponent |
View#super() |
视图销毁时 |
ServiceComponent |
Service#onCreate() |
Service#onDestroy() |
注意:
ActivityRetainedComponent
在配置更改后仍然存在,因此它在第一次调用Activity#onCreate()
时创建,在最后一次调用Activity#onDestroy()
时销毁。原因如上图
8_2 组件作用域
默认情况下,Hilt 中的所有绑定都未限定作用域。这意味着,每当应用请求绑定时,Hilt 都会创建所需类型的一个新实例。
下表列出了生成的每个组件的作用域注释:
Android 类 | 生成的组件 | 作用域 |
---|---|---|
Application |
ApplicationComponent |
@Singleton |
View Model |
ActivityRetainedComponent |
@ActivityRetainedScope |
Activity |
ActivityComponent |
@ActivityScoped |
Fragment |
FragmentComponent |
@FragmentScoped |
View |
ViewComponent |
@ViewScoped |
带有 @WithFragmentBindings 注释的 View |
ViewWithFragmentComponent |
@ViewScoped |
Service |
ServiceComponent |
@ServiceScoped |
8_3 组件层次结构
将模块安装到组件后,其绑定就可以用作该组件中的其他绑定依赖项,也可以用作组件层次结构中该组件下的任何子组件中的其他绑定依赖项:
[图片上传失败...(image-f7f169-1603855174131)]
注意:默认情况下,如果您在视图中执行字段注入,
ViewComponent
可以使用ActivityComponent
中定义的绑定。如果您还需要使用FragmentComponent
中定义的绑定并且视图是 Fragment 的一部分,应将@WithFragmentBindings
注释和@AndroidEntryPoint
一起使用。
9 Hilt 对不支持的类如何执行字段注入?
Hilt支持常见的Android类。但是,您可能需要在Hilt不支持的类中执行字段注入。比如我们创建的一个普通的class,此时我们可用过@EntryPoint
实现注入,如下:
@EntryPoint
class ExampleContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface ExampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
...
}
如果需要访问ExampleContentProviderEntryPoint
,可以使用EntryPointAccessors
,代码如下:
class ExampleContentProvider: ContentProvider() {
...
override fun query(...): Cursor {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val hiltEntryPoint =
EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)
val analyticsService = hiltEntryPoint.analyticsService()
...
}
}
注意:确保您以参数形式传递的组件和
EntryPointAccessors
静态方法都与@EntryPoint
接口上的@InstallIn
注释中的 Android 类匹配。如上我们使用
@InstallIn(ApplicationComponent::class)
,那么我们在获取时也要EntryPointAccessors.fromApplication(context?.applicationContext, XXXX::class.java)
, 保证context?.applicationContext,
和InstallIn
提供的是一致的。
~~~over~~~
reference:https://developer.android.google.cn/training/dependency-injection/hilt-android#component-lifetimes