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

简介: 本文详解Hilt在多模块Android项目中的实战落地,涵盖跨模块依赖注入、接口解耦、Component协调、测试Mock方案及常见问题排查,强调“接口定义在domain、实现分离于data、feature仅依赖抽象”的模块化设计原则

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
}
  1. 在 :data:repository 中实现接口:
// data/repository 模块
class UserRepositoryImpl @Inject constructor(
    private val api: UserApi
) : UserRepository {
    override suspend fun getUser(id: String): User {
        return api.fetchUser(id)
    }
}
  1. 在 :data:repository 的 Hilt Module 中绑定接口与实现:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    @Singleton
    abstract fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}
  1. 在 :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<User?>(null)
    val userState: StateFlow<User?> = _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. 使用 Provider<T> 或 Lazy<T> 延迟注入:
class A @Inject constructor(
    private val bProvider: Provider<B>
) {
    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 的依赖注入能力,可以让代码保持清晰、可测试、可维护。


推荐阅读:

相关文章
|
2月前
|
弹性计算 运维 安全
阿里云国际站(云老大):解决 VPC 对等连接网络不通,从配置校验到根因定位全套排查方法
云服务器之间明明配好了 VPC 对等连接,控制台也显示“已激活”,两台 ECS 之间的 ping 包却一去不回。这种“表面通了,实际不通”的情况在阿里云上并不罕见——往往不是服务故障,而是路由表、安全组或者网络 ACL 某个环节的配置出了遗漏。把排查路径梳理清楚,比频繁重建连接更治本。
194 0
|
存储 缓存 NoSQL
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
14196 1
|
2月前
|
JSON 安全 API
[Android 从零到一] Android 深度链接与 App Links:从 URI Scheme 到可验证的应用跳转
本文详解Android深度链接技术演进:从易被劫持的URI Scheme,到基于数字资产链接(assetlinks.json)验证的App Links方案。涵盖Manifest配置、签名指纹获取、验证流程、Navigation集成及Android 12适配,并总结常见坑点与最佳实践,助你实现安全、无缝的应用内跳转
204 0
|
3月前
|
缓存 监控 数据挖掘
Android ANR 定位与治理:从主线程阻塞到线上证据闭环
本文系统解析Android ANR成因与治理:厘清“未响应”非崩溃本质,聚焦主线程阻塞根因(锁竞争、I/O、Binder等),强调通过堆栈+Trace+指标构建线上证据闭环,并提供典型问题修复方案与工程化治理实践。
295 0
|
3月前
|
存储 自然语言处理 测试技术
[鸿蒙从零到一] HarmonyOS 通知与提醒实战:消息发布、点击跳转与定时触达
本文详解鸿蒙HarmonyOS通知与提醒实战:区分NotificationKit(即时通知)与ReminderAgent(定时提醒),涵盖权限申请、通知槽管理、点击跳转、进度更新、定时触发、ID去重、错误处理及真机测试要点,助开发者构建稳定可靠的生产级消息触达能力
555 0
|
3月前
|
存储 缓存 API
[鸿蒙从零到一] HarmonyOS 后台任务与定时能力实战:短时任务、长时任务与延迟调度
本文系统讲解HarmonyOS后台任务实战,涵盖短时任务(收尾保护)、长时任务(用户可感知持续运行)、延迟任务(Work Scheduler按条件调度)及代理提醒(准时通知),结合ArkTS代码、权限配置、异常处理与测试要点,助开发者构建稳定、合规、低耗的后台能力
326 0
|
3月前
|
测试技术 Android开发 iOS开发
Multiplatform:用一套声明式 UI 连接多端
Compose Multiplatform 是基于 Kotlin 的跨平台 UI 框架,延续 Jetpack Compose 声明式范式,支持 Android、iOS、桌面与 Web。它共享 UI 逻辑与状态,分层处理平台能力,适合工具类、中后台等 UI 高度一致的场景,助力 Android 团队高效拓展多端。(239 字)
256 0
|
6月前
|
传感器 人工智能 数据可视化
数字孪生项目的开发
数字孪生已超越3D展示,迈向实时感知、双向控制与仿真预测深度融合。本文详解2026年四层技术架构(感知→建模→机理→交互)、关键开发流程及生成式AI、具身智能等前沿亮点,覆盖工业、城市等多场景落地实践。(239字)
|
6月前
|
弹性计算 人工智能 前端开发
2026年阿里云新用户购买云服务器有没有优惠?新用户专属优惠政策及活动价格参考
2026年,阿里云为新用户提供多项优惠政策:新用户可抢购轻量应用服务器,2核2G配置仅38元/年,2核4G配置9.9元/月起;云服务器ECS特惠,2核2G配置99元/年,2核4G配置199元/年;通用算力型u2i实例享低至3折优惠。本文还为大家解释了新用户、老用户、产品新用户的定义,助用户精准把握优惠。建议用户购买前确认身份、按需选择产品、并关注活动规则,以更实惠的价格选购适合自己需求的云服务器。
799 6
|
8月前
|
人工智能 API 网络安全
2026年阿里云及本地部署OpenClaw(Clawdbot) 通过企业微信接入微信保姆级教程
2026年,OpenClaw(原Clawdbot)已经成为国内最流行的轻量化AI自动化代理工具,凭借低门槛、高扩展、多IM平台适配等优势,快速覆盖个人办公、团队协作、自动化任务处理等场景。很多用户希望把OpenClaw部署在稳定的云端,再通过企业微信接入个人微信,实现随时随地用微信发指令、AI自动执行任务。
4768 4