Jetpack架构组件库:Hilt_android hilt
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
Hilt
Hilt 是基于 Dagger2 的依赖注入框架Google团队将其专门为Android开发打造了一种纯注解的使用方式相比 Dagger2 而言使用起来更加简单。
依赖注入框架的主要作用就是控制反转IOC, Inversion of Control, 那么什么是控制反转呢
首先它是一种技术思想而不是一种具体的技术体现。它描述的是面向对象的开发语言领域中的对象的创建和管理问题。
- 控制指的是对象创建实例化、管理的权力
- 反转将控制权交给外部环境IOC框架
- 传统的开发方式一个类里面有很多成员变量对象这些成员对象都需要new出来
- IOC思想的开发方式IOC原则NO我们不要new这样耦合度太高入参改变所有引用都要改)而是通过IOC容器如Hilt来帮助我们实例化对象并赋值。
总结一句话就是一个类中创建对象是一种控制能力控制反转就是将这种创建对象的控制权转交给外部框架来自动实现。
常见的依赖注入手段
- 解决方案一配置xml文件里面标明哪个类用了哪些成员变量等需要加载这个类的时候我帮你注入new进去。Spring服务器开发常用IOC方案
- 解决方案二在需要注入的成员变量上添加注解例如
@Inject
在编译时生成相关的实现类运行时动态注入。(Android开发中常用的手段是 APT + JavaPoet 或者是 KSP + KotlinPoet)
Hilt 的使用
首先添加依赖项
首先将 hilt-android-gradle-plugin
插件添加到项目的根级 build.gradle
文件中
plugins {
...
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
然后应用 Gradle
插件并在 app/build.gradle
文件中添加以下依赖项
...
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
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
}
Hilt 使用 Java 8
功能。如需在项目中启用 Java 8
请将以下代码添加到 app/build.gradle
文件中
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Hilt 应用类
所有使用 Hilt
的应用都必须包含一个带有 @HiltAndroidApp
注解的 Application
类。
@HiltAndroidApp
会触发 Hilt
的代码生成操作生成的代码包括应用的一个基类该基类充当应用级依赖项容器。
生成的这一 Hilt
组件会附加到 Application
对象的生命周期并为其提供依赖项。此外它也是应用的父组件这意味着其他组件可以访问它提供的依赖项。
将依赖项注入 Android 类
在 Application
类中设置了 Hilt
且有了应用级组件后Hilt
可以为带有 @AndroidEntryPoint
注解的其他 Android
类提供依赖项
Hilt
目前支持以下 Android
类
- Application通过使用
@HiltAndroidApp
- ViewModel通过使用
@HiltViewModel
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
如果您使用 @AndroidEntryPoint
为某个 Android
类添加注解则还必须为依赖于该类的 Android
类添加注解。例如如果您为某个 fragment
添加注解则还必须为使用该 fragment
的所有 activity
添加注解。
注意在
Hilt
对Android
类的支持方面适用以下几项例外情况
Hilt
仅支持扩展ComponentActivity
的activity
如AppCompatActivity
。Hilt
仅支持扩展androidx.Fragment
的Fragment
。Hilt
不支持保留的fragment
。
@AndroidEntryPoint
会为项目中的每个 Android
类生成一个单独的 Hilt
组件。这些组件可以从它们各自的父类接收依赖项如组件层次结构中所述。
如需从组件获取依赖项请使用 @Inject
注解执行字段注入
注意由
Hilt
注入的字段不能为私有字段。尝试使用Hilt
注入私有字段会导致编译错误。
Hilt
注入的类可以有同样使用注入的其他基类。如果这些类是抽象类则它们不需要 @AndroidEntryPoint
注解。
如需详细了解 Android 类被注入的是哪个生命周期回调请参阅组件生命周期。
定义 Hilt 绑定
为了执行字段注入Hilt
需要知道如何从相应组件提供必要依赖项的实例。“绑定”包含将某个类型的实例作为依赖项提供所需的信息。
向 Hilt
提供绑定信息的一种方法是构造函数注入。在某个类的构造函数中使用 @Inject
注解以告知 Hilt
如何提供该类的实例
在一个类的代码中带有注解的构造函数的参数即是该类的依赖项。在本例中AnalyticsService
是 AnalyticsAdapter
的一个依赖项。因此Hilt
还必须知道如何提供 AnalyticsService
的实例。
注意在构建时
Hilt
会为Android
类生成Dagger
组件。然后Dagger
会走查您的代码并执行以下步骤
- 构建并验证依赖关系图确保没有未满足的依赖关系且没有依赖循环。
- 生成它在运行时用来创建实际对象及其依赖项的类。
Hilt 模块
有时类型不能通过构造函数注入。发生这种情况可能有多种原因。例如您不能通过构造函数注入接口。此外您也不能通过构造函数注入不归您所有的类型如来自外部库的类。在这些情况下您可以使用 Hilt 模块向 Hilt 提供绑定信息。
Hilt 模块是一个带有 @Module
注解的类。与 Dagger
模块一样它会告知 Hilt
如何提供某些类型的实例。与 Dagger
模块不同的是您必须使用 @InstallIn
为 Hilt
模块添加注解以告知 Hilt
每个模块将用在或安装在哪个 Android
类中。
注意Hilt 模块与 Gradle 模块不同。
您在 Hilt 模块中提供的依赖项可以在生成的所有与 Hilt 模块安装到的 Android 类关联的组件中使用。
注意由于
Hilt
的代码生成操作需要访问使用Hilt
的所有Gradle
模块因此编译Application
类的Gradle
模块还需要在其传递依赖项中包含您的所有Hilt
模块和通过构造函数注入的类。
使用 @Binds 注入接口实例
以 AnalyticsService
为例。如果 AnalyticsService
是一个接口则您无法通过构造函数注入它而应向 Hilt
提供绑定信息方法是在 Hilt
模块内创建一个带有 @Binds
注解的抽象函数。
@Binds
注解会告知 Hilt
在需要提供接口的实例时要使用哪种实现。
带有注解的函数会向 Hilt
提供以下信息
- 函数返回类型会告知
Hilt
该函数提供哪个接口的实例。 - 函数参数会告知
Hilt
要提供哪种实现。
Hilt 模块 AnalyticsModule
带有 @InstallIn(ActivityComponent::class)
注解因为您希望 Hilt
将该依赖项注入 ExampleActivity
。此注解意味着AnalyticsModule
中的所有依赖项都可以在应用的所有 activity
中使用。
使用 @Provides 注入实例
接口不是无法通过构造函数注入类型的唯一一种情况。如果某个类不归您所有因为它来自外部库如 Retrofit
、OkHttpClient
或 Room
数据库等类或者必须使用构建器模式创建实例也无法通过构造函数注入。
接着前面的例子来讲。如果 AnalyticsService
类不直接归您所有您可以告知 Hilt
如何提供此类型的实例方法是在 Hilt
模块内创建一个函数并使用 @Provides
为该函数添加注解。
带有注解的函数会向 Hilt
提供以下信息
- 函数返回类型会告知
Hilt
函数提供哪个类型的实例。 - 函数参数会告知
Hilt
相应类型的依赖项。 - 函数主体会告知
Hilt
如何提供相应类型的实例。每当需要提供该类型的实例时Hilt
都会执行函数主体。
为同一类型提供多个绑定
如果您需要让 Hilt
以依赖项的形式提供同一类型的不同实现必须向 Hilt
提供多个绑定。您可以使用限定符为同一类型定义多个绑定。
限定符是一种注解当为某个类型定义了多个绑定时您可以使用它来标识该类型的特定绑定。
仍然接着前面的例子来讲。如果需要拦截对 AnalyticsService
的调用您可以使用带有拦截器的 OkHttpClient
对象。对于其他服务您可能需要以不同的方式拦截调用。在这种情况下您需要告知 Hilt
如何提供两种不同的 OkHttpClient
实现。
首先定义要用于为 @Binds
或 @Provides
方法添加注解的限定符
然后Hilt
需要知道如何提供与每个限定符对应的类型的实例。在这种情况下您可以使用带有 @Provides
的 Hilt 模块。这两种方法具有相同的返回类型但限定符将它们标记为两个不同的绑定
您可以通过使用相应的限定符为字段或参数添加注解来注入所需的特定类型
最佳做法是如果您向某个类型添加限定符应向提供该依赖项的所有可能的方式添加限定符。让基本实现或通用实现不带限定符容易出错并且可能会导致 Hilt
注入错误的依赖项。
Hilt 中的预定义限定符
Hilt
提供了一些预定义的限定符。例如由于您可能需要来自应用或 activity
的 Context
类因此 Hilt
提供了 @ApplicationContext
和 @ActivityContext
限定符。
假设本例中的 AnalyticsAdapter
类需要 activity
的上下文。以下代码演示了如何向 AnalyticsAdapter
提供 activity
上下文
如需了解 Hilt 中提供的其他预定义绑定请参阅组件默认绑定。
为 Android 类生成的组件
对于您可以从中执行字段注入的每个 Android
类都有一个关联的 Hilt
组件您可以在 @InstallIn
注解中引用该组件。每个 Hilt
组件负责将其绑定注入相应的 Android
类。
前面的示例演示了如何在 Hilt 模块中使用 ActivityComponent
。
Hilt
提供了以下组件
注意
Hilt
不会为广播接收器生成组件因为Hilt
直接从SingletonComponent
注入广播接收器。
组件生命周期
Hilt
会按照相应 Android
类的生命周期自动创建和销毁生成的组件类的实例。
注意
ActivityRetainedComponent
在配置更改后仍然存在因此它在第一次调用Activity#onCreate()
时创建在最后一次调用Activity#onDestroy()
时销毁。
组件作用域
默认情况下Hilt
中的所有绑定都未限定作用域。这意味着每当应用请求绑定时Hilt
都会创建所需类型的一个新实例。
在本例中每当 Hilt
提供 AnalyticsAdapter
作为其他类型的依赖项或通过字段注入提供它如在 ExampleActivity
中时Hilt
都会提供 AnalyticsAdapter
的一个新实例。
不过Hilt
也允许将绑定的作用域限定为特定组件。Hilt
只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定对该绑定的所有请求共享同一实例。
下表列出了生成的每个组件的作用域注解
在本例中如果您使用 @ActivityScoped
将 AnalyticsAdapter
的作用域限定为 ActivityComponent
Hilt
会在相应 activity
的整个生命周期内提供 AnalyticsAdapter
的同一实例
注意将绑定的作用域限定为某个组件的成本可能很高因为提供的对象在该组件被销毁之前一直保留在内存中。请在应用中尽量少用限定作用域的绑定。如果绑定的内部状态要求在某一作用域内使用同一实例绑定需要同步或者绑定的创建成本很高那么将绑定的作用域限定为某个组件是一种恰当的做法。
假设 AnalyticsService
的内部状态要求每次都使用同一实例 - 不只是在 ExampleActivity
中而是在应用中的任何位置。在这种情况下将 AnalyticsService
的作用域限定为 SingletonComponent
是一种恰当的做法。结果是每当组件需要提供 AnalyticsService
的实例时都会提供同一实例。
以下示例演示了如何将绑定的作用域限定为 Hilt
模块中的某个组件。绑定的作用域必须与其安装到的组件的作用域一致因此在本例中您必须将 AnalyticsService
安装在 SingletonComponent
中而不是安装在 ActivityComponent
中
如需详细了解 Hilt 组件作用域请参阅 Android 和 Hilt 中的作用域限定。
注意如需详细了解使用 @ActivityRetainedScoped 或 @ViewModelScoped 限定作用域的区别请参阅 Hilt 和 Jetpack 集成文档中的 @ViewModelScoped 部分。
组件层次结构
将模块安装到组件后其绑定就可以用作该组件中其他绑定的依赖项也可以用作组件层次结构中该组件下的任何子组件中其他绑定的依赖项
注意默认情况下如果您在视图中执行字段注入
ViewComponent
可以使用ActivityComponent
中定义的绑定。如果您还需要使用FragmentComponent
中定义的绑定并且视图是Fragment
的一部分应将@WithFragmentBindings
注解和@AndroidEntryPoint
一起使用。
组件默认绑定
每个 Hilt
组件都附带一组默认绑定Hilt
可以将其作为依赖项注入您自己的自定义绑定。请注意这些绑定对应于常规 activity
和 fragment
类型而不对应于任何特定子类。这是因为Hilt
会使用单个 activity
组件定义来注入所有 activity
。每个 activity
都有此组件的不同实例。
还可以使用 @ApplicationContext
获得application
上下文绑定。例如
此外还可以使用 @ActivityContext
获得 activity
上下文绑定。例如
在 Hilt 不支持的类中注入依赖项
Hilt
支持最常见的 Android
类。不过您可能需要在 Hilt
不支持的类中执行字段注入。
在这些情况下您可以使用 @EntryPoint
注解创建入口点。入口点是由 Hilt
管理的代码与并非由 Hilt
管理的代码之间的边界。它是代码首次进入 Hilt
所管理对象的图的位置。入口点允许 Hilt
使用并非由 Hilt
管理的代码提供依赖关系图中的依赖项。
例如Hilt
并不直接支持 content provider
。如果您希望 content provider
使用 Hilt
来获取某些依赖项需要为所需的每个绑定类型定义一个带有 @EntryPoint
注解的接口并添加限定符。然后添加 @InstallIn
以指定要在其中安装入口点的组件如下所示
如需访问入口点请使用来自 EntryPointAccessors
的适当静态方法。参数应该是组件实例或充当组件持有者的 @AndroidEntryPoint
对象。确保您以参数形式传递的组件和 EntryPointAccessors
静态方法都与 @EntryPoint
接口上的 @InstallIn
注解中的 Android
类匹配
在本例中您必须使用 ApplicationContext
检索入口点因为入口点安装在 SingletonComponent
中。如果您要检索的绑定位于 ActivityComponent
中应改用 ActivityContext
。
Hilt 和 Dagger
Hilt
在依赖项注入库 Dagger
的基础上构建而成提供了一种将 Dagger
纳入 Android
应用的标准方法。
关于 Dagger
Hilt
的目标如下
- 简化
Android
应用的Dagger
相关基础架构。 - 创建一组标准的组件和作用域以简化设置、提高可读性以及在应用之间共享代码。
- 提供一种简单的方法来为各种
build
类型如测试、调试或发布配置不同的绑定。
由于 Android
操作系统会实例化它自己的许多框架类因此在 Android
应用中使用 Dagger
要求您编写大量的样板。Hilt
可减少在 Android
应用中使用 Dagger
所涉及的样板代码。Hilt
会自动生成并提供以下各项
- 用于将 Android 框架类与 Dagger 集成的组件 - 您不必手动创建。
- 作用域注解 - 与
Hilt
自动生成的组件一起使用。 - 预定义的绑定 - 表示 Android 类如 Application 或 Activity。
- 预定义的限定符 - 表示
@ApplicationContext
和@ActivityContext
。
Dagger
和 Hilt
代码可以共存于同一代码库中。不过在大多数情况下最好使用 Hilt
管理您在 Android
上对 Dagger
的所有使用。
将 Hilt 和其他 Jetpack 库一起使用
Hilt
包含可用于从其他 Jetpack
库提供类的扩展。Hilt
目前支持以下 Jetpack
组件
- ViewModel
- Navigation
- Compose
- WorkManager
您必须添加 Hilt
依赖项才能利用这些集成。
使用 Hilt 注入 ViewModel 对象
提供 ViewModel
方法是为其添加 @HiltViewModel
注解并在 ViewModel
对象的构造函数中使用 @Inject
注解。
然后带有 @AndroidEntryPoint
注解的 activity
或 fragment
可以使用 ViewModelProvider
或 by viewModels()
KTX 扩展照常获取 ViewModel
实例
@ViewModelScoped
所有 Hilt ViewModel
都由 ViewModelComponent
提供后者遵循与 ViewModel
相同的生命周期因此可以在配置更改后继续存在。如需将依赖项的作用域限定为 ViewModel
请使用 @ViewModelScoped
注解。
使用 @ViewModelScoped
类型后系统会在注入 ViewModel
的所有依赖项中提供限定了作用域的类型的单个实例。请求限定了作用域的类型的 ViewModel
的其他实例会收到其他实例。
如果需要在不同 ViewModel
之间共享单个实例则应使用 @ActivityRetainedScoped
或 @Singleton
限定其作用域。
与 Jetpack Navigation 库集成
请将下面这些额外的依赖项添加到 Gradle
文件中
dependencies {
...
implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'
}
如果 ViewModel
的作用域限定为导航图请使用 hiltNavGraphViewModels
函数该函数可与带有 @AndroidEntryPoint
注解的 fragment
搭配使用。
val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)
与 Jetpack Compose 集成
ViewModel 部分提及的 viewModel()
函数自动使用 Hilt
通过 @HiltViewModel
注解构造的 ViewModel
。
Hilt 和 Navigation
Hilt
还与 Navigation Compose
库集成。请将下面这些额外的依赖项添加到 Gradle
文件中
dependencies {
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
}
使用 Navigation Compose
时请始终使用 hiltViewModel
可组合函数获取带有 @HiltViewModel
注解的 ViewModel
的实例。该函数可与带有 @AndroidEntryPoint
注解的 fragment
或 activity
搭配使用。
例如如果 ExampleScreen
是导航图中的目的地请调用 hiltViewModel()
来获取作用域限定为该目的地的 ExampleViewModel
实例如以下代码段所示
如果您需要改为检索作用域限定为导航路线或导航图的 ViewModel
实例请使用 hiltViewModel
可组合函数并将相应的 backStackEntry
作为参数传递
使用 Hilt 注入 WorkManager
将下面这些额外的依赖项添加到 Gradle
文件中。请注意除了库之外您还需要添加一个额外的注释处理器它在 Hilt
注释处理器的基础上运行
dependencies {
...
implementation 'androidx.hilt:hilt-work:1.0.0'
// When using Kotlin.
kapt 'androidx.hilt:hilt-compiler:1.0.0'
// When using Java.
annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0'
}
注入一个 Worker方法是在类中使用 @HiltWorker
注解并在 Worker
对象的构造函数中使用 @AssistedInject
。您只能在 Worker
对象中使用 @Singleton
或未限定作用域的绑定。您还必须使用 @Assisted
为 Context
和 WorkerParameters
依赖项添加注解
然后让 Application
类实现 Configuration.Provider
接口注入 HiltWorkFactory
的实例并将其传入 WorkManager
配置如下所示
注意由于这会自定义
WorkManager
配置因此您还必须按照WorkManager
文档中指定的方法从AndroidManifest.xml
文件中移除默认的初始化程序。
Hilt组件库背后做了啥
添加了Hilt
相应注解的类会在build
目录下会生成对应的以Hilt_xxx
开头的java
类
可以看到对于 Activity
是在生成的 Hilt_xxxActivity
的构造方法中调用了 inject()
方法。 Hilt_xxxActivity
里面的逻辑很简单就是将当前对象强转成对应的子类对象然后保存到对应类型的Component中。
而对于 Application
则是在 onCreate()
方法中调用了 inject()
方法
import android.app.Application;
import androidx.annotation.CallSuper;
import dagger.hilt.android.internal.managers.ApplicationComponentManager;
import dagger.hilt.android.internal.managers.ComponentSupplier;
import dagger.hilt.android.internal.modules.ApplicationContextModule;
import dagger.hilt.internal.GeneratedComponentManagerHolder;
import dagger.hilt.internal.UnsafeCasts;
import java.lang.Object;
import java.lang.Override;
/**
* A generated base class to be extended by the @dagger.hilt.android.HiltAndroidApp annotated class. If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
*/
public abstract class Hilt_MyApp extends Application implements GeneratedComponentManagerHolder {
private boolean injected = false;
private final ApplicationComponentManager componentManager = new ApplicationComponentManager(new ComponentSupplier() {
@Override
public Object get() {
return DaggerMyApp_HiltComponents_SingletonC.builder()
.applicationContextModule(new ApplicationContextModule(Hilt_MyApp.this)).build();
}
});
@Override
public final ApplicationComponentManager componentManager() {
return componentManager;
}
@Override
public final Object generatedComponent() {
return this.componentManager().generatedComponent();
}
@CallSuper
@Override
public void onCreate() {
hiltInternalInject();
super.onCreate();
}
protected void hiltInternalInject() {
if (!injected) {
injected = true;
// This is a known unsafe cast, but is safe in the only correct use case:
// MyApp extends Hilt_MyApp
((MyApp_GeneratedInjector) generatedComponent()).injectMyApp(UnsafeCasts.<MyApp>unsafeCast(this));
}
}
}
但是这些都是一些抽象类那么在哪里实现的呢
在build目录下搜索会发现添加了相关注解的类都会生成对应类名的dex文件比如MainActivity会生成一个MainActivity.dex使用反编译工具打开对应的dex文件进行查看
可以看到我们自己写的业务类的父类被修改为继承了Hilt生成的Hilt_XXX相关的抽象类。
因此可知Hilt 会在编译器修改字节码修改了继承的父类为Hilt_XXXA相关的抽象类然后在父类中进行了强行注入处理逻辑。
IOC 的优缺点
Hilt 很好但是我们一定要在项目中使用它吗文章的最后我们来总结一下 IOC 的优缺点。
IOC 的优势
- 对象之间的耦合度降低
- 对象实例的创建变得比较容易管理很容易创建一个全局/局部的共享单例
- 场景模板代码创建实例对象全局或局部对象共享
IOC 的缺点
- 代码可读性较差不知道对象在哪里被创建的实例创建的时机在哪里入参是什么
- 增加新人学习成本和维护成本尤其是对团队的新成员而言对接手的新项目要熟悉整个IOC框架的运作模式才敢下手改代码。
- 额外生成大量的父类或方法来做IOC注入处理逻辑增加编译成本和App的体积加速触及65536方法数等
是否有必要引入 Hilt 或其他类似的 IOC?
- 通常来说这不是必须的取决于你的项目复杂程度。如果是简单的小项目使用它并不会变得更加简单反而可能会适得其反学习成本和工作收益不成正比。