Jetpack Hilt 框架的基本使用

news/2024/11/15 2:24:36/

什么是 Hilt?

Hilt 是一个功能强大、用法简单的依赖注入框架,于 2020 年加入到 Jetpack 家族中。它是 Android 团队联系了 Dagger2 团队,一起开发出来的一个专门面向 Android 的依赖注入框架。相比于 Dagger2,Hilt 最明显的特征就是简单,并且提供了 Android 专属的 API。

在项目中引入 Hilt

此部分以使用了 Java 17 的 Jetpack Compose 新项目为例,开发工具使用 Android Studio 2023.1.1 Canary 版本。信息截止 2023 年 5 月。

第一步,打开 gradle/libs.versions.toml 文件,加入 Hilt 的 Gradle 插件相关配置:

[versions]
hilt = "2.46.1"[plugins]
hiltAndroid = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

再打开项目的 build.gradle.kts 文件,引入插件:

plugins {alias(libs.plugins.hiltAndroid) apply false
}

第二步,在 libs.versions.toml 中加入 Hilt 的插件和依赖库:

[libraries]
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }

由于 Hilt 基于编译时注解实现,需要添加 kotlin-kapt 插件。在 app 的 build.gradle.kts 文件中再加入如下配置:

plugins {kotlin("kapt")
}android {compileOptions {// 这里设置为 Java 8 或以上即可sourceCompatibility = JavaVersion.VERSION_17targetCompatibility = JavaVersion.VERSION_17}
}dependencies {implementation(libs.hilt.android)kapt(libs.hilt.compiler)
}

现在,Hilt 已被成功引入到项目中。

Hilt 的基本用法

准备工作

使用 Hilt 时,必须自定义一个 Application 类,否则 Hilt 将无法正常工作。自定义的 Application 类中可以不写任何代码,但必须要加上 @HiltAndroidApp 注解。

@HiltAndroidApp
class MyApplication : Application() {
}

接下来将 MyApplication 注册到 AndroidManifest.xml 中:

<applicationandroid:name=".MainApplication">
</application>

准备工作到此已完成,接下来的任务是根据具体的业务逻辑使用 Hilt 进行依赖注入。

入口点

Hilt 简化了 Dagger2 的操作,使我们无需使用 @Component 注解编写桥接层逻辑,同时也限制了注入功能只能从几个 Android 固定的入口点开始:Application、Activity、Fragment、View、Service、BroadcastReceiver。
其中,只有 Application 入口点使用 @HiltAndroidApp 注解声明,其他所有入口点均使用 @AndroidEntryPoint 注解声明。例如,若希望在 Activity 中进行依赖注入,只需这样声明:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)}
}

不带参数的依赖注入

尝试定义一个类,在其构造函数上声明 @Inject 注解,如下:

class MusicPlayer() @Inject constructor() {fun init() {Log.d("MusicPlayer", "init")}
}

在 Activity 中注入,即可成功调用上面编写的 init() 方法:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {@Injectlateinit var musicPlayer: MusicPlayeroverride fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)musicPlayer.init()}
}

带参数的依赖注入

在上面的 MusicPlayer 类构造函数中加入一个 AudioDriver 参数,代表播放器组件依赖的系统音频驱动,如下所示:

class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {fun init() {Log.d("MusicPlayer", "init, audioDriver=$audioDriver")}
}

声明 AudioDriver 类时,也为其构造函数加上 @Inject 注解:

class AudioDriver @Inject constructor() {}

不需要再修改任何代码,即可成功调用 init() 方法,并成功打印 audioDriver 的 hashCode。

接口的依赖注入

定义一个 IDecoder 接口,代表播放音频时必备的音频解码器。接口中有两个待实现方法,分别用于创建解码器和销毁解码器、释放内存:

interface IDecoder {fun create()fun destroy()
}

实现用于解码 WAV 文件的 WavDecoder,在构造函数中加上 @Inject 注解:

class WavDecoder @Inject constructor() : IDecoder {override fun create() {Log.d("WavDecoder", "create")}override fun destroy() {Log.d("WavDecoder", "destroy")}
}

此外,再实现用于解码 MP3 文件的 Mp3Decoder,同样需要声明 @Inject 注解:

class Mp3Decoder @Inject constructor() : IDecoder {override fun create() {Log.d("Mp3Decoder", "create")}override fun destroy() {Log.d("Mp3Decoder", "destroy")}
}

新建一个抽象类,命名为 DecoderModule,在这个模块中通过定义抽象函数提供 IDecoder 接口所需要的实例:

@Module
@InstallIn(ActivityComponent::class)
abstract class DecoderModule {@Bindsabstract fun bindDecoder(wavDecoder: WavDecoder): IDecoder
}

修改 MusicPlayer 类中的代码,调用刚刚提供的解码器:

class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {@Injectlateinit var decoder: IDecoderfun init() {decoder.create()Log.d("MusicPlayer", "init, audioDriver=$audioDriver")decoder.destroy()}
}

此时再调用 init() 方法,即可看到 TAG 为 WavDecoder 的日志。

给相同类型注入不同实例

@Qualifer 接口用于给相同类型的类或接口注入不同的实例。分别定义两个注解,如下:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindWavDecoder@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindMp3Decoder

回到 DecoderModule 中,定义两个抽象函数,将刚才定义的两个注解分别添加到两个函数上方:

@Module
@InstallIn(ActivityComponent::class)
abstract class DecoderModule {@BindWavDecoder@Bindsabstract fun bindWavDecoder(wavDecoder: WavDecoder): IDecoder@BindMp3Decoder@Bindsabstract fun bindMp3Decoder(mp3Decoder: WavDecoder): IDecoder
}

回到 MusicPlayer 类,此时就可以让这个播放器同时支持两种格式的解码:

class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {@BindWavDecoder@Injectlateinit var wavDecoder: IDecoder@BindMp3Decoder@Injectlateinit var mp3Decoder: IDecoderfun init() {wavDecoder.create()mp3Decoder.create()Log.d("MusicPlayer", "init, audioDriver=$audioDriver")wavDecoder.destroy()mp3Decoder.destroy()}
}

第三方类的依赖注入

假如我们想在 MainActivity 中注入 OkHttpClient,该类由 OkHttp 提供,我们无法为其构造函数加上 @Inject 注解。这种情况下,需要借助 @Module 注解定义一个非抽象类,此处命名为 NetworkModule。
在该类中定义一个方法,加上 @Provides 注解,在函数体中提供一个 OkHttpClient 的实例,如下:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {@Providesfun provideOkHttpClient(): OkHttpClient {return OkHttpClient.Builder().connectTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).writeTimeout(20, TimeUnit.SECONDS).build()}
}

回到 MainActivity,使用 @Inject 注入 OkHttpClient,即可成功运行:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {@Injectlateinit var okHttpClient: OkHttpClient
}

为了方便开发者使用,我们在 NetworkModule 再给 Retrofit 类型提供实例,编写如下代码:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {@Providesfun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().addConverterFactory(GsonConverterFactory.create()).baseUrl("http://example.com/").client(okHttpClient).build()}
}

方法 provideRetrofit() 中的 okHttpClient 参数则会由 Hilt 自动使用 provideOkHttpClient() 方法进行创建。此时在 MainActivity 中再次尝试注入 Retrofit,也可以正常运行:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {@Injectlateinit var retrofit: Retrofit
}

Hilt 内置组件

使用 @Module 注入的类,需要使用 @InstallIn 注解指定注入的范围。Hilt 一共提供了 7 种组件类型,分别用于注入到不同的场景:

组件名注入范围
ApplicationComponentApplication
ActivityRetainedComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponent使用 @WithFragmentBindings 定义的 View
ServiceComponentService

若希望上方定义的 NetworkModule 可以在全项目中使用,只需这样修改:

@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
}

Hilt 组件作用域

Hilt 默认会为每次的依赖注入行为都创建不同的实例。对应前面的 7 个内置组件,Hilt 也提供了 7 种组件作用域注解,如下所示:

组件作用域对应内置组件
@SingletonApplicationComponent
@ActivityRetainedScopeActivityRetainedComponent
@ActivityScopedActivityComponent
@FragmentScopedFragmentComponent
@ViewScopedViewComponent
@ViewScopedViewWithFragmentComponent
@ServiceScopedServiceComponent

若希望 NetworkModule 中提供的 Retrofit 和 OkHttpClient 实例在全局只创建一份,只需加上 @Singleton 注解:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {@Singleton@Providesfun provideOkHttpClient(): OkHttpClient {return OkHttpClient.Builder().connectTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).writeTimeout(20, TimeUnit.SECONDS).build()}@Singleton@Providesfun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().addConverterFactory(GsonConverterFactory.create()).baseUrl("http://example.com/").client(okHttpClient).build()}
}

作用域注解也可以被直接声明到任何可注入类的上方,例如前面添加的 AudioDriver 类:

@Singleton
class AudioDriver @Inject constructor() {
}

这就表示 AudioDriver 在全局范围内都会共享同一个实例,且全局都可以对 AudioDriver 类进行依赖注入。

如上图所示,对某个类声明了某种作用域注解后,该注解的箭头所能指到的地方,都可以对该类进行依赖注入,同时在该范围内共享同一个实例。

预置 Qualifier

若前面定义的 AudioDriver 类需要一个 Context 参数,需要在该参数前加上一个 @ApplicationContext 注解,Hilt 会提供一个 Application 类型的 Context 给到 AudioDriver 类当中,代码即可编译通过:

@Singleton
class AudioDriver @Inject constructor(@ApplicationContext val context: Context) {
}

如果需要 Activity 或其他类型的 Context,使用 Hilt 预置的另外一种 Qualifier 即可:

@Singleton
class AudioDriver @Inject constructor(@ActivityContext val context: Context) {
}

此时编译代码会报错,因为 AudioDriver 类是 Singleton 的,与 Qualifier 的范围不匹配。
对于 Activity 和 Application 这两个类型,Hilt 为它们预置好了注入功能。如果某个类依赖于 Activity 或 Application,不需要添加任何注解,Hilt 可以自动识别,如下:

class AudioDriver @Inject constructor(val application: Application) {
}class AudioDriver @Inject constructor(val activity: Activity) {
}

注意必须是 Application 和 Activity 这两个类型,即使声明它们的子类型,编译都无法通过。

HiltViewModel 的使用

先在 libs.versions.toml 中声明相关依赖,如下:

[versions]
hilt-lifecycle-viewmodel = "1.0.0-alpha03"[libraries]
androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-lifecycle-viewmodel" }

在 build.gradle.kts 中添加依赖:

dependencies {kapt(libs.androidx.hilt.compiler)
}

通过 @HiltViewModel 注解提供一个 ViewModel:

@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
}

然后,带有 @AndroidEntryPoint 注解的 Activity 或 Fragment 即可使用 ViewModelProvider 或 by viewModels() 扩展照常获取 ViewModel 实例:

class MainActivity : AppCompatActivity() {private val mainViewModel: MainViewModel by viewModels()
}


http://www.ppmy.cn/news/110200.html

相关文章

RabbittMQ快速实战和集群架构

介绍对比: Kafka&#xff1a;topic不能太多&#xff0c;一个缺点&#xff0c;影响Kafka的吞吐量 集群搭建&#xff1a;【单个也是一个集群&#xff08;特殊&#xff09;】 集群搭建&#xff1a;https://blog.csdn.net/p393975269/article/details/129830252 1&#xff1a;默认…

电源工程师必备技能汇总

一、掌握常用拓扑结构 1.反激 2.全桥/半桥 3.倍流 4.PFC电路 5.Flyback 6.LLC 7.Buck 8.维也纳整流 9.光伏逆变器 10.并网逆变器 11.两电平整流器 12.三电平整流器 二、掌握三款电力仿真软件--对于上述拓扑结构的仿真控制 1.PSIM 2.MATLAB 3.Plecs 三、熟练运用…

20230529_Hadoop_集群操作命令

HDFS_集群操作命令&#xff1a; 一、集群启停命令 # 启动Hadoop的HDFS进程start-dfs.sh# 关闭Hadoop的HDFS进程stop-dfs.sh# 单独关闭某一个进程hadoop-daemon.sh start[/stop] namenode[/datanode/secondarynamenode]二、HDFS文件系统的基本信息 数据的路径表达方式&#xff…

大学四年,因为这8个网站,我成为同学眼中的学霸

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 大学期间&#xff0c;几乎每一个教过我的老师都反应&#xff0c;我的学习态度不好&#x…

C语言——每日一题

1.倒置字符串 倒置字符串 要将每一个单词逆序输出&#xff0c;首先可以将整个字符串内容都逆序输出&#xff0c;然后再将字符串中的每一个单词再进行逆序。 例如&#xff1a;逆序 i like beijing. 先逆序成&#xff1a;.gnijieb ekil i 再将每个单词逆序&#xff1a; beij…

ElasticSearch安装部署

ElasticSearch安装部署 简介 全文搜索属于最常见的需求&#xff0c;开源的 Elasticsearch &#xff08;以下简称 es&#xff09;是目前全文搜索引擎的首选。 它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它。 Elasticsearch简称es&…

LC-1130. 叶值的最小代价生成树(贪心、区间DP、单调栈)

1130. 叶值的最小代价生成树 难度中等272 给你一个正整数数组 arr&#xff0c;考虑所有满足以下条件的二叉树&#xff1a; 每个节点都有 0 个或是 2 个子节点。数组 arr 中的值与树的中序遍历中每个叶节点的值一一对应。每个非叶节点的值等于其左子树和右子树中叶节点的最大…

.NET 8 Preview 4 发布

作者&#xff1a;Jon Douglas - Principal Program Manager, NuGet 翻译&#xff1a;Alan Wang 排版&#xff1a;Alan Wang 我们很高兴与大家分享在 .NET 8 预览版 4 中的所有新功能和改进&#xff01;这次发布是继预览版 3之后的更新。您将在这些月度发布中看到更多功能逐渐亮…