Hilt 是 Google 官方推荐的 Android 依赖注入 (Dependency Injection, DI) 框架,它基于 Dagger(另一个 DI 框架),但专门为 Android 优化,简化了配置和使用。
对于熟悉 Spring DI 的开发者来说,理解 Hilt 最大的不同在于:Spring 框架在启动时会扫描和管理整个应用中所有需要注入的 Bean,而在 Android 中,DI 框架(如 Hilt)需要与 Android 组件(如Activity, Fragment, Service)的生命周期深度集成。
与 SpingDI核心思想对比
- Spring DI: 你通常在类上使用
@Component``@Service``@Repository等注解来声明 Bean,然后通过@Autowired或构造函数注入。Spring 容器会处理整个应用级别的 Bean 依赖关系。 - Hilt: 你需要先声明一个 Application 级别的组件容器,然后指定在 Android 组件(
Activity,Fragment等)中进行注入。Hilt 使用注解来告诉它如何创建和提供依赖,以及在哪里进行注入。
模拟一个简单场景:一个餐厅点餐应用,需要一个Cashier(收银员)来处理付款,Cashier依赖于一个PaymentService(支付服务)。
Info
对于 Module 的疑惑解答:
Spring 的「Module」就是整个 IoC 容器本身;容器启动时会把所有
@Component… 一股脑儿扫描进来,自动帮你建 Bean。\ Hilt 的「Module」则是一个 显式声明 的“配方”类:只有写在**@Configuration**@Service@Module + @InstallIn里的东西才会被编译期收集到对应作用域的容器里,否则 Hilt 不会自动扫描,即必须显式声明。
Spring:启动时会扫描 classpath,把标注了
@Component``@Service``@Mapper等的类,动态注册到 Bean 容器。Hilt:编译期用注解处理器 (APT, KAPT) 生成依赖图,只有显式声明过的类才会放进容器,不会做全盘扫描。
为什么不和 Spring 一样放在启动时扫描,而必须显式声明?
- 性能差异
- Spring:服务端 JVM 环境,启动时 classpath 扫描成本可接受
- Android:App 冷启动非常敏感(100~200ms 以内),不可能像 Spring 那样全量扫描。所以 Hilt 用 APT 在编译期生成容器代码,避免运行时扫描
- 限制更严格
- Spring 容器是单一的 ApplicationContext
- Hilt 容器是分层级的 Component Tree(Application → Activity → Fragment → View → …),每个作用域需要精确控制,自动扫描容易引入不可控的依赖关系
Hilt
@Module
@Module用于标记一个类为”依赖提供模块”,该模块负责提供依赖项的创建逻辑。
@Provides
- 用途:用于为不能通过构造函数注入的类型(如第三方库类)添加绑定
- 方法类型:必须是具体实现的方法(非抽象)
- 参数:可以有参数,用于提供依赖项
- 返回类型:要提供实例的类型
- 执行方式:每次需要该实例时都会执行方法体
@Provides
@Singleton
fun providePopupFactory(): PopupFactory {
return PopupFactory()
}@Binds
- 用途:用于绑定接口和其实现(特别是当接口没有构造函数时)
- 方法类型:必须是抽象方法
- 参数:只能是实现类的参数
- 返回类型:接口类型
- 执行方式:Hilt会直接使用实现类,不会执行方法体
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindsAnalyticsService(analyticsServiceImpl: AnalyticsServiceImpl): AnalyticsService
}注入模块内的类
@HiltAndroidApp
通过@HiltAndroidApp启用 Hilt 依赖注入。
**作用:**当@HiltAndroidApp注解Application类时,Hilt 会自动生成一个基类,并接管整个应用的依赖注入。它会为整个应用组件提供一个依赖容器,这样应用中的其他组件(如 Activity、Fragment 等)就能通过 Hilt 获得它们需要的依赖项。
/**
* Application 类
*
* @author zhangzhen40@baidu.com
* @version
* @since 2025/08/18
*/
@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var appManager: AppManager
override fun onCreate() {
super.onCreate()
}
override fun onTerminate() {
super.onTerminate()
appManager.unregisterReceiver()
}
}@Singleton
@Singleton注解来自javax.inject包,是一个标准的 Java 依赖注入注解,Hilt 框架也支持它。
作用:可以确保一个类的实例在整个应用生命周期内只被创建一次。当 Hilt 看到一个类被@Singleton注解时,它会把这个类的实例放在应用的全局容器中,这样每次请求该依赖时,都会返回同一个单例实例。跟 Spring 默认单例一样。
@ApplicationContext
是一个 Hilt 特有的限定符注解(Qualifier)。
作用: 解决依赖注入中的歧义。当你的应用中可能存在不同类型的Context(例如,应用级的ApplicationContext和 Activity 级别的ActivityContext)时,Hilt 需要知道你想要注入哪一个。@ApplicationContext就是告诉 Hilt,你想要的是应用级别的上下文,它与应用的生命周期相同。
/**
* App 监听管理类
*
* Hilt 里字段注入只能使用在 Hilt 管理的组件里(Activity、Fragment..这些组件由 Android 管理生命周期)
* 普通类只能通过构造器注入的方式
*
* @author zhangzhen40@baidu.com
* @version
* @since 2025/08/20
*/
@Singleton
class AppManager @Inject constructor(
@ApplicationContext private val context: Context,
private val packageChangeReceiver: PackageChangeReceiver
) {
/** 注册状态 */
private var isRegistered = false
/**
* 注册应用安装/卸载广播
*/
fun registerReceiver() {
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
context.registerReceiver(packageChangeReceiver, intentFilter)
isRegistered = !isRegistered
}
/**
* 反注册应用安装/卸载广播
*/
fun unregisterReceiver() {
if (!isRegistered) {
return
}
context.unregisterReceiver(packageChangeReceiver)
isRegistered = !isRegistered
}
// TODO: 冷启嗅探
fun addListener(listener: IAppListener) {
packageChangeReceiver.addListener(listener)
}
fun removeListener(listener: IAppListener) {
packageChangeReceiver.removeListener(listener)
}
}@Inject constructor(...)
通过@Inject constructor告诉 Hilt 如何去创建这个类的实例并注入其依赖。
作用:干两件事儿,1. 告诉 Hilt,这个类的构造函数是用来注入依赖的。当你使用 @Inject constructor 时,Hilt 会检查构造函数中的所有参数,并尝试从它的依赖容器中为这些参数提供实例。2. 告诉 Hilt 如何去创建类的实例。
/**
* 包变更广播接收器
*
* @author zhangzhen40@baidu.com
* @version
* @since 2025/08/20
*/
class PackageChangeReceiver @Inject constructor(): BroadcastReceiver() {
private val listeners = mutableSetOf<IAppListener>()
fun addListener(listener: IAppListener) {
listeners.add(listener)
}
fun removeListener(listener: IAppListener) {
listeners.remove(listener)
}
override fun onReceive(context: Context?, intent: Intent?) {
val action = intent?.action ?: return
val packageName = intent.data?.schemeSpecificPart ?: return
when (action) {
Intent.ACTION_PACKAGE_ADDED -> {
listeners.forEach { it.onAppInstall(packageName) }
}
Intent.ACTION_PACKAGE_REMOVED -> {
listeners.forEach { it.onAppUninstall(packageName) }
}
}
}
}@AndroidEntryPoint
作用是标记一个 Android 组件(如 Activity、Fragment、Service、Application、BroadcastReceiver)为依赖注入的入口,以便 Hilt 能够自动为其及其依赖项生成和注入所需的对象。在某个组件中使用@Inject注入依赖时,需要在该类上加上@AndroidEntryPoint。
如果忘记在组件上加@AndroidEntryPoint,直接使用@Injecte注入时,会报类似如下错误:
Hilt does not know how to provide an instance of ...注意点
在 Hilt 里,字段注入 (@Inject lateinit var …)只能出现在 Hilt 管理的组件,@AndroidEntryPoint 标记的Activity``Fragment``Service``BroadcastReceiver,以及 Hilt 支持的ViewModel里。
Hilt 不会主动去 new 一个不在自身管理范围的类,所以不能随便在一个普通 Kotlin 类里写字段注入。
注入第三方 SDK
当你想注入一个第三方 SDK 中的类时,你不能直接在那个类的构造函数上添加 @Inject 注解,因为你没有权限修改 SDK 的源代码。
在这种情况下,你需要显式地告诉 Hilt 如何创建这个类的实例。通常,有两种主要方式来做到这一点:
文档
1. 使用@Provides注解的方法
这是最常见也最推荐的方式。你可以在一个 Module(模块)类中创建一个方法,并用@Provides注解来声明这个方法会“提供”一个特定类型的依赖。
步骤:
- 创建一个用
@Module注解的 Kotlinobject或class(声明图结构) - 在其中创建一个方法,用
@Provides注解标记它 - 该方法的返回类型就是你想提供的依赖类型(例如,第三方 SDK 中的
ThirdPartyService) - 方法的参数就是这个依赖所需要的其他依赖项,Hilt 会自动注入这些参数
示例:
假设你有一个名为Retrofit的第三方库,你需要一个Retrofit实例来使用它。
@Module
@InstallIn(SingletonComponent::class) // 告诉 Hilt 这个模块的作用范围
object NetworkModule {
@Provides // 告诉 Hilt,这个方法提供了 Retrofit 的实例
@Singleton // 如果你希望这个实例是单例的
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
}
}现在,Hilt 知道当有其他类需要Retrofit实例时,它应该调用NetworkModule.provideRetrofit()方法来获取它。
2. 使用@Binds注解的抽象方法
如果你想注入的是一个接口,并且有一个具体的实现类,可以使用@Binds注解。
步骤:
- 创建一个用
@Module注解的抽象类 - 创建一个用
@Binds注解的抽象方法 - 方法的参数是你想要注入的实现类,返回类型是你想要注入的接口。
示例:
假设你的应用有一个AnalyticsService接口,但你使用了FirebaseAnalytics这个第三方实现。
interface AnalyticsService {
fun trackEvent(eventName: String)
}
class FirebaseAnalyticsService @Inject constructor() : AnalyticsService {
override fun trackEvent(eventName: String) {
// 使用 Firebase SDK 追踪事件
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Binds // 告诉 Hilt,当你需要 AnalyticsService 接口时,请使用 FirebaseAnalyticsService
@Singleton
abstract fun bindAnalyticsService(impl: FirebaseAnalyticsService): AnalyticsService
}现在,当你向任何类注入AnalyticsService接口时,Hilt 都会自动提供FirebaseAnalyticsService 的实例。
实际例子 - 多模块调用
**案例:**在hilt-bestpractices 模块引用popupwindow-bestpractices中的PopupFactory来创建 PopupWindow。
- 在被引用模块的
build.gradle中的 application 改为 library,声明在打包时以 aar 的方式引用。并删除 applicationId
plugins {
id("com.android.library")
alias(libs.plugins.kotlin.android)
}
defaultConfig {
// pplicationId "com.example.popupwindow_bestpractices"
minSdk 24
targetSdk 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}@HiltAndroidApp启用 Hilt 依赖注入,并在AndroidManifest.xml中声明 Application 的 name
/**
* MuApp Class
*
* @author zhangzhen40@baidu.com
* @version
* @since 2025/08/27
*/
@HiltAndroidApp
class MyApp :Application(){
}<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".MyApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>- 声明
PopupFactory.class如何创建
/**
* 引入 PopupWindow 模块
*
* @author zhangzhen40@baidu.com
* @version
* @since 2025/08/23
*/
@Module
@InstallIn(SingletonComponent::class)
object PopupWindowModule {
@Provides
@Singleton
fun providePopupFactory(): PopupFactory {
return PopupFactory
}
}- 通过
@AndroidEntryPoint声明@Inject注入对象实例
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var popupFactory: PopupFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val openPopupWindowTextview = findViewById<TextView>(R.id.open_popup_window_textview)
openPopupWindowTextview.setOnClickListener {
val popup = popupFactory.create(PopupType.BlankPopup, this, PopupModel())
popup.show()
}
}
}也可以把 Module 模块放在某个被引用的包中,相当于对外暴露有个可以依赖注入的接口(告诉调用方怎么创建对象)。这种情况说明你有权限可以直接更改第三方的代码库,只需要在需要依赖注入的类上打上注解即可, 引用的地方直接@Inject即可,代码如下:
QA 问题:
Q:@Module是什么?为什么需要@Module?
A:上面解释过 Hilt 等这类 Android 的依赖注入工具是在编译期做 DI,那么就需要一个东西来显式声明,这个东西就是@Module。相当于在 Hilt 的SingletonComponent依赖图中,显式声明一条从IPopupService 到PopupServiceImpl的绑定规则,并指定其作用域为@Singleton。
等价的抽象语义是:
Component: SingletonComponent
Node: IPopupService
Provider: PopupServiceImpl
Scope: Singleton
Rule:
IPopupService ← PopupServiceImpl所以编译期,Hilt 记住了这个类型映射。当依赖图中出现IPopupService节点时,就会使用PopupServiceImpl作为其 concrete provider。
为什么需要@Module:
- 将依赖项的创建逻辑集中管理,使代码更清晰、更易于维护
- 避免在多个类中重复编写创建依赖项的代码
- 允许将依赖项的创建与使用解耦,实现更好的可测试性
- 可以控制依赖项的生命周期(如@Singleton)
Q:这样看一个应用写一个 Moudle 就够了?
A:不是的,一个应用通常需要多个Module,而不是只写一个。
- 模块化:将不同功能的依赖项分组管理,使代码结构更清晰
- 关注点分离:网络、数据库、UI组件等不同部分的依赖项应该分开管理
- 复用性:不同模块可以被不同组件使用
- 可维护性:当需要修改某个依赖项的提供方式时,只需修改对应的Module
Q:这个写法是固定的吗?抽象方法?然后方法参数是实现类?
A:@Binds的写法是“结构上固定、形式上可选”。
| 维度 | @Binds | @Provides |
|---|---|---|
| 是否抽象 | 必须 | 不需要 |
| 是否有方法体 | ❌ | ✅ |
| 是否允许逻辑 | ❌ | ✅ |
| 生成代码 | 更少 | 更多 |
| 推荐场景 | 接口→实现 | 构造 / 条件 / 工厂 |
Q:如果多个实现类,必须声明多个方法吗?
是的,如果有多个实现类,通常需要为每个实现类提供一个@Provides方法。但需要区分这些实现类,通常使用@Qualifier注解:
// 定义不同的Qualifier
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultPopup
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class CustomPopup
// 在Module中提供多个实现
@Module
@InstallIn(SingletonComponent::class)
object PopupWindowModule {
@Provides
@Singleton
@DefaultPopup
fun provideDefaultPopupFactory(): PopupFactory {
return DefaultPopupFactory()
}
@Provides
@Singleton
@CustomPopup
fun provideCustomPopupFactory(): PopupFactory {
return CustomPopupFactory()
}
}在使用时,通过@Qualifier指定要注入的实现类:
@Inject
@DefaultPopup
lateinit var defaultPopupFactory: PopupFactory
@Inject
@CustomPopup
lateinit var customPopupFactory: PopupFactoryQ:****@Provides方法和@Binds什么区别?
A:第三方库(无法通过修改代码来支持依赖注入)的,需要通过@Provides为第三方类或需要复杂创建逻辑的类提供绑定
- 如果你有接口和实现类,优先使用@Binds,因为它更简洁且不涉及实例创建逻辑
- 如果你无法修改第三方库的类,或者需要复杂创建逻辑,使用@Provides
厂内 pyramid
厂内也有一个依赖注入框架,叫pyramid,引入代码如下
// 依赖注入
implementation "com.baidu.pyramid:pyramid-annotation:${LIB_NADCORE_PYRAMID_DI_ANNOTATION_VERSION}"
implementation "com.baidu.searchbox.pyramid:service:${LIB_NADCORE_PYRAMID_SERVICE_VERSION}"差异点
其实使用的方法差不多,只不过大部分的时候依赖注入都是用来注入第三方依赖,或者厂内 SDK,需要显式的写出具体构造的方法,所以有了AdRuntimeHolder.class。我理解,这个类其实就等同于 Hilt 的 Module,用来显式的声明如何把其他的类加载到容器中。
@Autowired
public class AdRuntimeHolder {
/** debug标签 */
public static final boolean DEBUG = AppConfig.isDebug();
/**
* 获取 IAdRuntime 单例对象。
* @return IAdRuntime 单例
*/
@Inject(force = false)
@NonNull
public static IAdRuntime getAdRuntime() {
// 依赖注入框架会将 null 替换为AdRuntime的单例对象
return IAdRuntime.EMPTY;
}
/**
* 获取 IAdDownloader 单例对象
* @return IAdDownloader 单例
*/
@Inject(force = false)
@Singleton
@NonNull
public static IAdDownloader getAdDownloader() {
return IAdDownloader.EMPTY;
}
}如上代码
Hilt 等依赖通常通过构造函数注入或者字段注入,调用时写@Inject lateinit var xxx。但是这里的依赖通过静态方法 + 注解暴露出来,调用时就像普通工具类调用:
IAdRuntime runtime = AdRuntimeHolder.getAdRuntime();一些猜测:
@Autowired标记类- 表明这个类要交给 IoC 容器管理。
- 类似于 Spring 的
@Component/ Dagger 的@Module。
@Inject(force = false)标记方法- 框架会扫描这些静态方法,把返回值(本来是占位的 EMPTY)替换成真正的实现类单例。
force = false可能表示:如果 IoC 容器里没有这个依赖,不要报错(可选依赖)。
- 返回的是接口类型(
IAdRuntime、IAdDownloader)- 对外只暴露接口,底层实现由框架在运行时或编译期注入。
- 如果没有实现,就返回
EMPTY(一个空实现,避免 NullPointerException)。
@Singleton- 标识容器注入时保持单例。
“静态 Holder + EMPTY 默认实现 + IoC 框架注入” 的完整形态。
我们来逐行拆解:
/**
* 短小融合一跳广告参数存储接口
*
* @return INadVideoFlowSchemeParamsProxy
*/
@Inject(force = false) // 表示依赖注入(如果容器里没有实现,也不会报错,用默认值兜底)
@Singleton // 注入的对象保持单例(全局唯一)
@NonNull // 不允许返回 null,至少保证 EMPTY
public static INadVideoFlowSchemeParamsProxy getVideoFlowSchemeParamsProxy() {
return INadVideoFlowSchemeParamsProxy.Companion.getEMPTY();
}📌 运行机制(和AdRuntimeHolder一致)
- 定义接口
interface INadVideoFlowSchemeParamsProxy {
fun getAdResource(nid: String): String?
fun saveAdResource(nid: String?, adResource: String?)
fun clearCache()
companion object {
val EMPTY = object : INadVideoFlowSchemeParamsProxy {
override fun getAdResource(nid: String) = null
override fun saveAdResource(nid: String?, adResource: String?) = Unit
override fun clearCache() = Unit
}
}
}- Holder 暴露静态方法
@Autowired
public class NadVideoFlowSchemeHolder {
@Inject(force = false)
@Singleton
@NonNull
public static INadVideoFlowSchemeParamsProxy getVideoFlowSchemeParamsProxy() {
// 默认返回 EMPTY(空实现)
return INadVideoFlowSchemeParamsProxy.Companion.getEMPTY();
}
}- 容器启动时扫描注解
- 框架会发现
@Inject标记的静态方法。 - 如果 IoC 容器里已经有
INadVideoFlowSchemeParamsProxy的真实实现,就会替换掉EMPTY。 - 如果没有实现(可能广告 SDK 没有接入),就保留
EMPTY,调用时返回null或Unit,不会崩溃。
- 框架会发现
- 调用方用起来很简单
INadVideoFlowSchemeParamsProxy proxy = NadVideoFlowSchemeHolder.getVideoFlowSchemeParamsProxy();
proxy.saveAdResource("123", "{广告数据}");
String ad = proxy.getAdResource("123");
proxy.clearCache();👉 不需要知道背后是用 SP 存储、数据库存储还是走内存缓存。
使用
整个流程分为 3个角色:
① 实现类(lib_ad_runtime 仓库)
@Service
@Singleton
class NadVideoFlowSchemeParamsProxy: INadVideoFlowSchemeParamsProxy {
...
}@Service注解会告诉 IoC 容器:
“当有人需要INadVideoFlowSchemeParamsProxy时,可以用这个实现类NadVideoFlowSchemeParamsProxy来提供。”@Singleton说明 IoC 框架内部会保证全局只创建一个实例。
也就是说,这里定义了「提供方」。
② 接口(lib_ad_runtime 仓库)
interface INadVideoFlowSchemeParamsProxy { ... }- 只是规范,不带任何注解。
- 同时定义了一个
EMPTY静态对象,作为 兜底空实现。
③ 使用方(lib_ad 模块)
@Inject(force = false)
@Singleton
@NonNull
public static INadVideoFlowSchemeParamsProxy getVideoFlowSchemeParamsProxy() {
return INadVideoFlowSchemeParamsProxy.Companion.getEMPTY();
}@Inject注解告诉 IoC 框架:
“这里需要一个INadVideoFlowSchemeParamsProxy实例,请注入。”force = false表示如果找不到实现类,就用方法体里的默认返回值(这里就是EMPTY)。- 这就是你写的 AdRuntimeHolder,作用相当于 统一入口,其他业务模块只要调用:
AdRuntimeHolder.getVideoFlowSchemeParamsProxy()就能拿到 IoC 框架注入的真实实现类(NadVideoFlowSchemeParamsProxy)。
如果注入失败,就退回 EMPTY。
🧩 链路总结
- 定义接口:
INadVideoFlowSchemeParamsProxy - 提供实现(带
@Service):NadVideoFlowSchemeParamsProxy - 提供入口(带
@Inject):AdRuntimeHolder.getVideoFlowSchemeParamsProxy() - 业务调用:直接调
AdRuntimeHolder.getVideoFlowSchemeParamsProxy().saveAdResource(...)
所以:
lib_ad_runtime是 提供实现的仓库lib_ad是 对外暴露 IoC 入口的仓库- IoC 框架在编译或运行时会扫描
@Service,把实现类注册到容器里。 - 业务侧永远不直接
new,而是通过AdRuntimeHolder拿到代理实例。
👉 也就是说,AdRuntimeHolder 里的 @Inject 方法,其实就是 “注入点 + fallback 兜底”,而不是 Hilt 的 @Provides。
更新: 2026-01-09 19:15:18
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/ez1wh7mn15laau72