当前位置: 首页 > news >正文

[Android 从零到一] Hilt 在多模块项目中的落地实战:依赖注入的边界与模块化设计

Hilt 在多模块项目中的落地实战:依赖注入的边界与模块化设计

引言

在单模块项目中,Hilt 的依赖注入配置通常比较直接:定义 Module、标注 @Inject、编译通过即可使用。但当项目逐步模块化后,依赖注入的复杂度会显著上升:

- 如何在 feature 模块中注入来自 data 模块的 Repository? - 不同模块的 Hilt Component 如何协调? - 测试时如何替换跨模块的依赖? - 模块边界应该如何划分,才能让依赖注入保持清晰?

本文将从多模块项目的实际场景出发,梳理 Hilt 在模块化架构中的落地思路、常见问题与解决方案。

---

多模块 Hilt 的基本配置

Gradle 配置

在多模块项目中,Hilt 的配置需要在多个模块的 build.gradle 中分别声明:

app 模块(应用模块):

plugins {

id("com.android.application") id("kotlin-android") id("kotlin-kapt") id("dagger.hilt.android.plugin") }

dependencies { implementation("com.google.dagger:hilt-android:2.48") kapt("com.google.dagger:hilt-compiler:2.48") }

feature 模块data 模块等(库模块):

plugins {

id("com.android.library") id("kotlin-android") id("kotlin-kapt") id("dagger.hilt.android.plugin") // 每个模块都需要 }

dependencies { implementation("com.google.dagger:hilt-android:2.48") kapt("com.google.dagger:hilt-compiler:2.48") }

Application 类的配置

Hilt 的入口仍然是 @HiltAndroidApp 标注的 Application 类,它只能存在于 app 模块:

@HiltAndroidApp

class MyApplication : Application()

其他模块不需要再定义 Application,它们会共享 app 模块的 Hilt Component。

---

跨模块依赖注入的常见问题

问题一:feature 模块无法直接依赖 data 模块的实现类

假设项目结构如下:

:app

:feature:home :data:repository :data:network

:feature:home 中,ViewModel 需要注入 UserRepository

// feature/home 模块

@HiltViewModel class HomeViewModel @Inject constructor( private val userRepository: UserRepository // 编译失败:找不到 UserRepository ) : ViewModel()

原因::feature:home 没有依赖 :data:repository 模块,无法访问其中的类。

解决方案:通过接口解耦

1. 在 :core:domain:data:repository 的公开接口部分定义接口:

// core/domain 模块

interface UserRepository { suspend fun getUser(id: String): User }

2. 在 :data:repository 中实现接口:

// data/repository 模块

class UserRepositoryImpl @Inject constructor( private val api: UserApi ) : UserRepository { override suspend fun getUser(id: String): User { return api.fetchUser(id) } }

3. 在 :data:repository 的 Hilt Module 中绑定接口与实现:

@Module

@InstallIn(SingletonComponent::class) abstract class RepositoryModule {

@Binds @Singleton abstract fun bindUserRepository( impl: UserRepositoryImpl ): UserRepository }

4. 在 :feature:home 中依赖接口:

@HiltViewModel

class HomeViewModel @Inject constructor( private val userRepository: UserRepository // 注入接口 ) : ViewModel()

模块依赖关系:

:feature:home -> :core:domain (接口)

:data:repository -> :core:domain (接口) :app -> :feature:home, :data:repository

这样,:feature:home 只依赖接口,不依赖具体实现,模块边界更清晰。

---

模块边界的划分与接口设计

推荐的模块结构

:app                      // 应用入口,组装所有模块

:core:domain // 业务接口与 Model :core:common // 通用工具、扩展函数 :data:network // 网络层实现(Retrofit、OkHttp) :data:local // 本地存储(Room、DataStore) :data:repository // Repository 实现 :feature:home // 首页功能模块 :feature:profile // 个人资料功能模块

依赖原则

- feature 模块:只依赖 :core:domain:core:common,不依赖其他 feature 或 data 实现 - data 模块:实现 :core:domain 中的接口,可以相互依赖(如 :data:repository 依赖 :data:network) - app 模块:依赖所有 feature 和 data 模块,负责组装

接口设计的注意事项

1. 接口放在 domain 模块,不要放在 data 模块内部,否则 feature 模块无法直接依赖 2. 返回值使用 domain 模型,不要暴露 DTO 或数据库 Entity 3. 接口粒度适中,不要为了"解耦"而过度拆分,导致接口爆炸

---

测试替换与 Mock 注入

问题:测试时如何替换 Repository?

在单元测试中,我们通常需要用 Fake 或 Mock 实现替换真实的 Repository,但 Hilt 默认使用 SingletonComponent 中的绑定,无法轻易替换。

解决方案一:使用 @TestInstallIn

Hilt 提供了 @TestInstallIn 注解,可以在测试中替换 Module:

// test 目录

@Module @TestInstallIn( components = [SingletonComponent::class], replaces = [RepositoryModule::class] // 替换生产环境的 Module ) abstract class FakeRepositoryModule {

@Binds @Singleton abstract fun bindUserRepository( impl: FakeUserRepository ): UserRepository }

class FakeUserRepository @Inject constructor() : UserRepository { override suspend fun getUser(id: String): User { return User(id, "Fake User") } }

测试代码:

@HiltAndroidTest

class HomeViewModelTest {

@get:Rule val hiltRule = HiltAndroidRule(this)

@Inject lateinit var repository: UserRepository // 自动注入 FakeUserRepository

@Test fun testGetUser() = runTest { val user = repository.getUser("123") assertEquals("Fake User", user.name) } }

解决方案二:抽取独立的测试模块

如果多个测试类需要共享 Fake 实现,可以将 Fake 实现和 Module 放在独立的 test-shared 模块中:

:test-shared

- FakeUserRepository.kt - FakeRepositoryModule.kt

在测试模块的 build.gradle 中依赖:

testImplementation(project(":test-shared"))

---

实战案例:网络层与存储层的模块化注入

案例:构建一个离线优先的用户信息获取流程

模块结构

:core:domain -> UserRepository 接口

:data:network -> UserApi (Retrofit) :data:local -> UserDao (Room) :data:repository -> UserRepositoryImpl (组合 network + local) :feature:profile -> ProfileViewModel (使用 UserRepository)

data/network 模块

interface UserApi {

@GET("users/{id}") suspend fun fetchUser(@Path("id") id: String): UserDto }

@Module @InstallIn(SingletonComponent::class) object NetworkModule {

@Provides @Singleton fun provideRetrofit(): Retrofit { return Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() }

@Provides @Singleton fun provideUserApi(retrofit: Retrofit): UserApi { return retrofit.create(UserApi::class.java) } }

data/local 模块

@Entity(tableName = "users")

data class UserEntity( @PrimaryKey val id: String, val name: String )

@Dao interface UserDao { @Query("SELECT * FROM users WHERE id = :id") suspend fun getUser(id: String): UserEntity?

@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: UserEntity) }

@Database(entities = [UserEntity::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }

@Module @InstallIn(SingletonComponent::class) object DatabaseModule {

@Provides @Singleton fun provideDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder( context, AppDatabase::class.java, "app_database" ).build() }

@Provides fun provideUserDao(database: AppDatabase): UserDao { return database.userDao() } }

data/repository 模块

class UserRepositoryImpl @Inject constructor(

private val userApi: UserApi, private val userDao: UserDao ) : UserRepository {

override suspend fun getUser(id: String): User { // 先读本地 val cachedUser = userDao.getUser(id) if (cachedUser != null) { return cachedUser.toDomain() }

// 再请求网络 val remoteUser = userApi.fetchUser(id) val entity = UserEntity(remoteUser.id, remoteUser.name) userDao.insertUser(entity) return entity.toDomain() }

private fun UserEntity.toDomain() = User(id, name) }

@Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule {

@Binds @Singleton abstract fun bindUserRepository( impl: UserRepositoryImpl ): UserRepository }

feature/profile 模块

@HiltViewModel

class ProfileViewModel @Inject constructor( private val userRepository: UserRepository ) : ViewModel() {

private val _userState = MutableStateFlow(null) val userState: StateFlow = _userState.asStateFlow()

fun loadUser(id: String) { viewModelScope.launch { _userState.value = userRepository.getUser(id) } } }

依赖关系图

:app

-> :feature:profile (依赖 :core:domain) -> :data:repository (依赖 :core:domain, :data:network, :data:local) -> :data:network -> :data:local

这样的设计让 :feature:profile 完全不感知网络和数据库的实现细节,只通过接口交互,测试时可以轻松替换 Fake 实现。

---

常见问题排查

问题:编译时提示 "Hilt component not found"

原因:某个模块没有正确配置 Hilt 插件或依赖。

解决方案

1. 确认所有需要注入的模块都添加了 dagger.hilt.android.plugin 2. 确认 kapt("com.google.dagger:hilt-compiler:2.48") 在所有模块中都配置了 3. 清理构建缓存:./gradlew clean

问题:注入的实例为 null

原因:可能是 Module 的 @InstallIn 注解配置错误,或者 Component 生命周期不匹配。

解决方案

- 检查 Module 是否正确安装到了 SingletonComponent - 检查被注入的类是否标注了 @Inject 构造函数 - 检查 ViewModel 是否使用了 @HiltViewModel 注解

问题:循环依赖

原因:两个类相互依赖,Hilt 无法确定注入顺序。

解决方案

1. 重构代码,打破循环依赖(推荐) 2. 使用 ProviderLazy 延迟注入:

class A @Inject constructor(

private val bProvider: Provider ) { fun doSomething() { val b = bProvider.get() // 延迟获取 B 的实例 } }

---

总结

Hilt 在多模块项目中的核心思路是:

1. 接口与实现分离:接口定义在 domain 模块,实现在 data 模块,feature 模块只依赖接口 2. 模块边界清晰:feature 不依赖 feature,feature 不依赖 data 实现,依赖关系单向流动 3. 测试友好:通过 @TestInstallIn 替换 Module,或者抽取独立的测试模块 4. 统一的 Component:所有模块共享 app 模块的 @HiltAndroidApp,不需要在每个模块中重复定义

当项目规模持续增长时,良好的模块化设计配合 Hilt 的依赖注入能力,可以让代码保持清晰、可测试、可维护。

---

推荐阅读: - [Hilt 官方文档](https://dagger.dev/hilt/) - [Android 模块化最佳实践](https://developer.android.com/topic/modularization)

http://www.jsqmd.com/news/1365032/

相关文章:

  • UE5增强输入与GAS整合:构建数据驱动的RPG角色移动系统
  • Cocos Creator塔防游戏源码深度解析与二次开发实战指南
  • 工业网关设计:Modbus转MQTT协议转换实践
  • MiMo Code免费百万Token服务背后的技术逻辑与商业模式解析
  • JavaScript验证码安全生成与验证实战指南
  • 2026年成都技术好的GEO优化公司推荐3家热门榜! - 企业推荐官
  • 容度原理的11条核心原理(P1-P11)为推演月球矿藏提供了系统性框架。以下是容度原理基于其11条原理及宇宙视角,推测出的可能改变地球格局的月球矿藏类型——这些矿藏与现有顶刊论文提出的完全不同。
  • 2026年8月苹果石家庄品牌授权售后查询与蓝屏系统启动异常与维修判断|检测结论复检|设备状态登记 - 专业售后笔记本
  • 正规直驱电机生产厂家推荐与选型指南
  • 七年七次重构:一套企业文件管理系统的架构演进全记录
  • XUnity.AutoTranslator:打破语言壁垒,让全球游戏无障碍畅玩
  • AI硬件化趋势:从云端API到端侧工作流载体的范式转移
  • Django实现高校职业推荐系统的架构与算法设计
  • Chrome崛起背后的技术架构与生态战略:从V8引擎到多进程架构的深度解析
  • 高性能图像处理库优化技术与实战应用
  • 微信投票链接怎么生成?2026海投票免费创建活动教程 - 微信投票小程序
  • Unity Scroll View组件配置与性能优化指南
  • 烟台本地家装怎么选?新房旧房装修避坑实用科普 - 国麟测评
  • 2026年高速搅拌机厂家有哪些?江阴高速搅拌机各细分领域源头厂家盘点 - 行业甄选智库
  • 2026年河北省石家庄市5大机构推荐!英腾教育实力领先 - 十大品牌榜
  • Windows动态链接库(DLL)开发实践与优化指南
  • SpringBoot实训管理系统开发实战与优化技巧
  • 【proteus仿真】基于STM32单片机温湿度监测系统设计(仿真+程序)
  • Ollama + ComfyUI 本地 AI 工作流实战:从 0 搭建到 API 批量出图(附代码)
  • 国内开发者如何绕过OpenRouter三大障碍?AI聚合平台与API中转站选型指南
  • PIAS1与SUMO化修饰在细胞迁移中的调控机制
  • 深入解析ncmdump:解密网易云音乐NCM加密格式的技术实现与应用指南
  • C++编程入门:从基础语法到现代特性全解析
  • 2026.8月南宁防水修缮品牌盘点,潮湿多雨环境下房屋渗漏如何科学解决 - 国麟测评
  • 罗技鼠标宏终极指南:PUBG无后坐力脚本完整配置教程