Hilt 依赖注入详解
Hilt 依赖注入详解
面试高频指数:高 依赖注入(DI)是大型项目标配,Hilt 是 Android 官方 DI 框架。
1. 为什么需要依赖注入
// ✗ 手动创建依赖:耦合、难测试
public class UserViewModel {
private final ApiService api = new ApiService(new OkHttpClient()); // 硬编码依赖
private final UserDao dao = AppDatabase.getInstance(this).userDao();
}// ✗ 手动创建依赖:耦合、难测试
class UserViewModel {
private val api = ApiService(OkHttpClient()) // 硬编码依赖
private val dao = UserDao(AppDatabase.getInstance(this))
}问题:
- 依赖创建逻辑分散,难以替换(如测试 Mock)。
- 生命周期管理混乱(单例 vs 每次新建)。
- 代码难维护、难测试。
DI 方案:依赖的创建由容器统一管理,使用时注入。
DI 带来的三个直接收益:① 可测试性——测试时只需替换容器中的绑定(如把网络换成 Fake),被测类无需改动;② 可维护性——依赖集中在 Module 中声明,一目了然;③ 生命周期可控——结合作用域,单例、页面级、请求级对象各得其所,避免内存泄漏。
2. Hilt 与 Dagger 的关系
- Dagger:Google 的编译期 DI 框架(Java/Kotlin),功能强大但配置繁琐。
- Hilt:基于 Dagger 的 Android 专用封装,自动生成大量样板代码。
- 原理相同:都是编译期代码生成(APT/KSP),无运行时反射,性能零损耗。
@Inject / @Module / @Component
│ 编译期
▼
Dagger 生成 DaggerXxxComponent(手写 DI 代码)
│
Hilt 自动集成 Android 组件生命周期这张图揭示了 Hilt 的本质:Dagger 负责"编译期生成 DI 代码",Hilt 负责"把这些代码接入 Android 组件"。开发者只写注解,生成、接线全部由框架完成,这也是 Hilt 被称为"Android 专用 DI 框架"的原因。
3. 基础用法
3.1 开启 Hilt
@HiltAndroidApp
public class MyApplication extends Application {
}@HiltAndroidApp
class MyApplication : Application()3.2 注入 Activity/Fragment
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
@Inject
UserRepository repository; // 字段注入
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// repository 已可用
}
}@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: UserRepository // 字段注入
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// repository 已可用
}
}3.3 构造注入
// 依赖可构造时,直接 @Inject 构造
public class UserRepository {
private final ApiService api;
@Inject
public UserRepository(ApiService api) {
this.api = api;
}
}// 依赖可构造时,直接 @Inject 构造
class UserRepository @Inject constructor(
private val api: ApiService
)4. Module 与 Provides
当依赖需要配置(第三方库、接口实现)时用 Module:
@Module
@InstallIn(SingletonComponent.class) // 安装位置
public class NetworkModule {
// OkHttpClient 单例(object → static 方法)
@Provides
@Singleton
public static OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build();
}
// Retrofit 接口
@Provides
@Singleton
public static ApiService provideApiService(OkHttpClient client) {
return new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService.class);
}
}
@Module
@InstallIn(SingletonComponent.class)
public abstract class RepositoryModule {
// 接口绑定(抽象)
@Binds
@Singleton
public abstract UserRepository bindUserRepository(UserRepositoryImpl impl);
}@Module
@InstallIn(SingletonComponent::class) // 安装位置
object NetworkModule {
// OkHttpClient 单例
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build()
// Retrofit 接口
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService =
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
// 接口绑定(抽象)
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}@Provides 与 @Binds 的分工值得强调:前者在方法体内编写创建逻辑(适合 Retrofit、OkHttp 这类需要配置的第三方对象),后者则是纯声明式绑定(接口 → 有 @Inject 构造的实现类),方法体为空、效率更高。一个模块里两种写法可以并存:需要配置的用 @Provides,接口实现映射用 @Binds。
5. 作用域(Scope)
作用域的本质是给绑定加上生命周期限制:标注 @Singleton 的绑定在整个 Application 存活期间只有一份实例,标注 @ActivityScoped 的绑定则在 Activity 销毁时随之释放。选择作用域的唯一依据是"依赖的存活时间应该和哪个组件一致":
| 作用域 | 存活时间 | 安装位置 |
|---|---|---|
@Singleton | Application 生命周期 | SingletonComponent |
@ActivityScoped | Activity 生命周期 | ActivityComponent |
@FragmentScoped | Fragment 生命周期 | FragmentComponent |
@ViewModelScoped | ViewModel 生命周期 | ViewModelComponent |
@ServiceScoped | Service 生命周期 | ServiceComponent |
匹配原则:作用域必须与安装组件匹配——例如
@ActivityScoped的绑定只能放在@InstallIn(ActivityComponent.class)的模块中,放在 SingletonComponent 中会直接编译报错。另外,作用域只对"标注了作用域的绑定"生效:同一类型若无作用域注解,则每次注入都会新建实例。一个经典误区是"以为@Singleton是全局唯一,其实它只是被 SingletonComponent 持有"。
@ActivityScoped
public class SessionManager {
@Inject
public SessionManager() {
// ...
}
}@ActivityScoped
class SessionManager @Inject constructor() { ... }注意:作用域必须与安装组件匹配(
@ActivityScoped不能放在 SingletonComponent 的 Module 中)。
6. ViewModel 注入
给 ViewModel 注入依赖有两条路径:传统方式需要手写 ViewModelProvider.Factory,把 Repository 等依赖手动传进去,代码繁琐;Hilt 方式只需给 ViewModel 加上 @HiltViewModel,构造参数用 @Inject 标注,Hilt 就会自动生成工厂。Fragment/Activity 端无需关心工厂细节,ViewModelProvider(this).get(...) 或 by viewModels() 即可拿到已注入依赖的实例:
@HiltViewModel
public class UserListViewModel extends ViewModel {
private final UserRepository repository;
@Inject
public UserListViewModel(UserRepository repository) {
this.repository = repository;
}
}
// 使用
@AndroidEntryPoint
public class UserListFragment extends Fragment {
private UserListViewModel viewModel;
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
viewModel = new ViewModelProvider(this).get(UserListViewModel.class); // Hilt 自动创建
}
}@HiltViewModel
class UserListViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
val users: StateFlow<List<User>> = repository.observeUsers()
}
// 使用
@AndroidEntryPoint
class UserListFragment : Fragment() {
private val viewModel: UserListViewModel by viewModels() // Hilt 自动创建
}7. 测试支持
Hilt 的测试支持围绕"替换依赖"展开:@TestInstallIn(replaces = NetworkModule.class) 让测试模块在测试环境中顶替生产模块,把真实的网络实现换成内存 Fake;@HiltAndroidTest + HiltAndroidRule 则保证测试运行时依赖图被完整构建并注入到测试类中。这样测试既不需要 Mock 框架,也能验证真实装配关系:
// 替换测试模块
@Module
@TestInstallIn(
components = SingletonComponent.class,
replaces = NetworkModule.class
)
public class FakeNetworkModule {
@Provides
@Singleton
public static ApiService provideApiService() {
return new FakeApiService();
}
}
// 或手动构造
@HiltAndroidTest
public class UserRepositoryTest {
@Rule
public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
@Before
public void setUp() {
hiltRule.inject();
}
}// 替换测试模块
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NetworkModule::class]
)
object FakeNetworkModule {
@Provides
@Singleton
fun provideApiService(): ApiService = FakeApiService()
}
// 或手动构造
@HiltAndroidTest
class UserRepositoryTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Before
fun setUp() {
hiltRule.inject()
}
}8. 高频面试题
Q1:Hilt 和 Dagger 的区别? A:Hilt 是 Dagger 的 Android 封装:① 自动生成 Component,无需手写; ② 提供 @AndroidEntryPoint 等组件注入;③ 自动集成 Jetpack(ViewModel、WorkManager); ④ 提供测试支持(@TestInstallIn)。底层原理相同(编译期代码生成)。
Q2:@Provides 和 @Binds 的区别? A:@Provides:在方法中创建实例(可自定义逻辑、配置第三方库); @Binds:绑定接口到实现(实现类本身有 @Inject 构造),要求方法为抽象且只有一个参数。
Q3:Hilt 是怎么在编译期生成代码的? A:通过 APT/KSP 处理注解:解析 @HiltAndroidApp 生成 Hilt_MyApplication, 解析 @AndroidEntryPoint 生成 Hilt_MainActivity(重写 onCreate,先调用 inject() 注入字段再执行子类逻辑),解析 Module 生成 Dagger Component 实现。
Q4:什么时候不该用 Hilt? A:① 极小型项目(引入成本大于收益);② 大量动态创建的对象(作用域难管理); ③ 反射强依赖场景(Hilt 是编译期静态的)。合理评估后再引入。
Q5:@ViewModelScoped 和 @Singleton 的区别? A:@ViewModelScoped 的实例随 ViewModel 销毁(如每屏一个的缓存); @Singleton 全局唯一(如 Retrofit、数据库)。用错作用域会导致内存泄漏或状态错乱。
9. 小结
- Hilt = Dagger + Android 集成,编译期代码生成,无反射损耗。
- 核心注解:
@HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@Provides、@Binds。 - 作用域决定生命周期,ViewModel 注入用
@HiltViewModel。 - 面试重点:与 Dagger 关系、编译期生成原理、作用域选择。