Jetpack Hilt 框架的基本使用
什么是 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 种组件类型,分别用于注入到不同的场景:
| 组件名 | 注入范围 |
|---|---|
| ApplicationComponent | Application |
| ActivityRetainedComponent | ViewModel |
| ActivityComponent | Activity |
| FragmentComponent | Fragment |
| ViewComponent | View |
| ViewWithFragmentComponent | 使用 @WithFragmentBindings 定义的 View |
| ServiceComponent | Service |
若希望上方定义的 NetworkModule 可以在全项目中使用,只需这样修改:
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
}
Hilt 组件作用域
Hilt 默认会为每次的依赖注入行为都创建不同的实例。对应前面的 7 个内置组件,Hilt 也提供了 7 种组件作用域注解,如下所示:
| 组件作用域 | 对应内置组件 |
|---|---|
| @Singleton | ApplicationComponent |
| @ActivityRetainedScope | ActivityRetainedComponent |
| @ActivityScoped | ActivityComponent |
| @FragmentScoped | FragmentComponent |
| @ViewScoped | ViewComponent |
| @ViewScoped | ViewWithFragmentComponent |
| @ServiceScoped | ServiceComponent |
若希望 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()
}
相关文章:
Jetpack Hilt 框架的基本使用
什么是 Hilt? Hilt 是一个功能强大、用法简单的依赖注入框架,于 2020 年加入到 Jetpack 家族中。它是 Android 团队联系了 Dagger2 团队,一起开发出来的一个专门面向 Android 的依赖注入框架。相比于 Dagger2,Hilt 最明显的特征就…...
exec()在不同namespace执行结果的区别
记录一个很tricky的问题,下面这段code在执行func1时会出现NameError: name List is not defined,但执行func2时一切正常。 import typescontent """ from typing import Listclass GeneratedData:qna: List"""def func1…...
人工智能革命中的22个隐藏职业:推动科技行业的变革
作者 | Manas Sadangi 随着人工智能技术的不断发展,它正在创造一系列前所未有的就业机会。虽然数据科学家、机器学习工程师和人工智能研究人员等传统的人工智能角色得到了广泛认可,但在推动科技行业变革方面,还有一些鲜为人知的职业同样重要。…...
算法题3 — 求字符串中的最长子串
文章目录 题目示例示例1示例2示例3 解题解法1解法2 leetcode 题目 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 示例 示例1 输入: s “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例…...
【FreeRTOS】——中断优先级设置中断相关寄存器临界段代码保护调度器挂起与恢复
目录 前言: 一、中断优先级设置 二、中断相关寄存器(STM32-Cortex M3) 三、临界段代码保护 四、任务调度器的挂起和恢复 总结: 前言: 博客笔记根据正点原子视频教程编辑,仅供学习交流使用࿰…...
1.2 什么是eBPF?(下)
四,eBPF的优势 4.1 eBPF程序的动态加载 eBPF程序可以动态地加载到内核中,或从内核中删除。这个要与内核模块的加载与卸载区分开来。这里顺便讨论下eBPF程序与内核模块的区别,如下: 而Linux内核模块是面向内核API编程的,可以直接运行在内核当中。eBPF程序是面向BPF体系结构…...
掌握哪些测试技术才能说自己已经学成了?
一、过硬的基础能力 其实所有的测试大佬都是从底层基础开始的,随着时间,经验的积累慢慢变成大佬。要想稳扎稳打在测试行业深耕,成为测试大牛,首当其冲的肯定就是拥有过硬的基础,所有的基础都是根基,后期所…...
什么是C语言?
C语言是一种高级编程语言,于1972年由Dennis Ritchie在贝尔实验室开发出来。它是一种通用的、结构化的编程语言,被广泛用于系统软件、嵌入式系统、游戏开发以及科学计算等领域。 C语言的设计目标是提供一种简洁、高效、可移植的编程语言,以便…...
SAP-物料主数据-质量管理视图字段解析
过账到质检库存:要勾选,否则收货后库存不进入质检库存HU检验:收货到启用HU管理的库位时产生检验批,例如某个成品物料是收货到C002库位,该库位启用了HU管理,那么此处要勾选。但是如果勾选了,却收…...
TOP RPA·脱普×实在丨日用品企业脱普签约实在智能,构建全域数据智能运营系统
近日,实在智能与脱普日用化学品(中国)有限公司(简称“脱普企业”)在脱普企业上海总部举行“全域数据智能运营”项目启动会,双方领导及项目组关键成员共同参会,就项目目标、实施进程、沟通机制等…...
【Android】Handler(四)Looper的相关知识点
Handler 机制是 Android 多线程间通信的一种常见方式。每个 Handler 对象由一个 Looper 和一个 MessageQueue 组成,用于将 Message 对象处理到指定的线程中。通过创建 Handler 实例,在子线程中创建 Message 对象并通过sendMessage()方法发送给 Handler&a…...
Redis缓存雪崩及解决办法
缓存雪崩 1.缓存雪崩是指在同- -时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到 达数据库,带来巨大压力。 2.解决方案: ◆给不同的Key的TTL添加随机值 ◆利用Redis集群提高服务的可用性 ◆给缓存业务添加降级限流策略 降级可做为系统的保底…...
Maven私服仓库配置-Nexus详解
目录 一、什么是Maven私服?二、Maven 私服优势三、Maven 私服搭建四、Sonatype Nexus介绍五、Nexus仓库属性和分类六、Nexus仓库配置以及创建仓库七、Nexus配置用户角色八、Maven SNAPSHOT(快照)九、项目当中配置Nexus上传依赖十、项目当中配置Nexus下载依赖十一、测…...
Systrace系列10 —— Binder 和锁竞争解读
本文主要是对 Systrace 中的 Binder 和锁信息进行简单介绍,简单介绍了 Binder 的情况,介绍了 Systrace 中 Binder 通信的表现形式,以及 Binder 信息查看,SystemServer 锁竞争分析等。 Binder 概述 Android 的大部分进程间通信都使用 Binder,这里对 Binder 不做过多的解释…...
React Hooks中使用useState异步回调获取不到最新值的问题
ReactHook中useState异步回调获取不到最新值及解决⽅案 预先了解 setState 的两种传参⽅式 1、直接传⼊新值 setState(options); 列如: const [state, setState] useState(0); setState(state 1); 2、传⼊回调函数 setState(callBack); 例如: …...
JavaScript 高级 (完结)
目录 深浅拷贝 浅拷贝 深拷贝 递归实现深拷贝 js库lodash里面cloneDeep内部实现了深拷贝 JSON序列化 异常处理 throw 抛异常 try /catch 捕获异常 debugg 处理this this指向 普通函数 箭头函数 改变this call() apply() bind() call apply bind 总结 性能优化…...
【P30】JMeter 事务控制器(Transaction Controller)
文章目录 一、事务控制器(Transaction Controller)参数说明二、测试计划设计2.2.1、勾选 Generate parent sample2.2.1、勾选 Include duration of timer and pre-post processors in generated sample 一、事务控制器(Transaction Controlle…...
【MySQL】MySQL的事务原理和实现?
文章目录 MySQL事务的底层实现原理一、事务的目的可靠性和并发处理 二、实现事务功能的三个技术2.1 redo log 与 undo log介绍2.1.1 redo log2.1.2undo log 2.2 mysql锁技术2.2.1 mysql锁技术 2.3 MVCC基础 三、事务的实现3.1 原子性的实现3.1.1 undo log 的生成3.1.2 根据undo…...
S7-300Smart1200的ISO on TCP通信
1、西门子PLC的通信资源 1.1 S7-1200 的PROFINET 通信口 S7-1200 CPU 本体上集成了一个 PROFINET 通信口,支持以太网和基于 TCP/IP 的通信标准。使用这个通信口可以实现 S7-1200 CPU 与编程设备的通信,与HMI触摸屏的通信,以及与其它 CPU 之间的通信。这个PROFINET 物理接口…...
Spark写入Hive报错Mkdir failed on :com.alibaba.jfs.JindoRequestPath
1. 报错内容 23/05/31 14:32:13 INFO [Driver] FsStats: cmdmkdirs, srcoss://sync-to-bi.[马赛克].aliyuncs.com/tmp/hive, dstnull, size0, parameterFsPermission:rwx-wx-wx, time-in-ms32, version3.5.0 23/05/31 14:32:13 ERROR [Driver] ApplicationMaster: User class …...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...
【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...
