第十五章:Hilt多模块项目:多模块架构下的依赖管理、模块间依赖隔离

多模块架构,说白了就是把一个巨型App拆成若干个独立的小模块。每个模块各司其职,互不干扰。这样做的好处很明显——编译快了、团队协作顺了、代码复用也方便了。

但问题来了:依赖怎么管理?

模块A要用模块B里的Repository,模块C又依赖模块D的ViewModel。如果处理不好,依赖关系就会乱成一锅粥。我在项目中就见过一个惨案——两个模块互相依赖,编译时直接报循环依赖错误,排查了整整两天。

嗯,今天我们就来聊聊,在Hilt的多模块项目中,如何优雅地管理依赖,以及如何实现模块间的依赖隔离。

15.1 多模块依赖管理的核心原则

我个人习惯把多模块依赖管理归纳为三条原则:

  • 单向依赖:模块只能依赖比自己更「底层」的模块,不能反向依赖
  • 接口隔离:模块之间通过接口通信,不暴露具体实现
  • 按需提供:每个模块只提供自己负责的依赖,不越界

你想想看,如果每个模块都遵守这三条,依赖关系就会像一棵树,清晰明了。而不是一张蜘蛛网,牵一发而动全身。

核心思想:依赖管理不是「怎么注入」,而是「怎么设计模块边界」。Hilt只是工具,架构设计才是根本。

15.2 模块分层与依赖方向

我一般把模块分为三层:

层级 模块示例 依赖方向
应用层 app、feature-home、feature-profile 依赖业务层和基础层
业务层 data、domain、repository 依赖基础层
基础层 core、common、network、database 不依赖任何业务模块

举个例子:feature-home 模块可以依赖 data 模块,但 data 模块绝对不能依赖 feature-home。一旦出现反向依赖,你的项目就会陷入「改一个模块,所有模块都要重新编译」的噩梦。

我曾经接手过一个项目,基础层居然依赖了业务层的某个工具类。结果每次改业务代码,基础层都要重新编译,整个构建时间从3分钟变成了15分钟。嗯,后来我花了整整一周才把这种循环依赖清理干净。

15.3 Hilt中的模块间依赖隔离

Hilt本身提供了几种机制来实现依赖隔离。我来逐一说说。

15.3.1 使用@Module和@InstallIn限定作用域

每个模块的Hilt配置,都应该限定在自己的作用域内。比如:

// 在 :data 模块中
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    @Singleton
    fun provideUserRepository(
        api: UserApi,
        dao: UserDao
    ): UserRepository {
        return UserRepositoryImpl(api, dao)
    }
}

这样,UserRepository 只在 :data 模块内部提供。其他模块如果想用,必须通过接口来获取,而不是直接引用 UserRepositoryImpl

小技巧:把接口定义在 :domain:core 模块中,实现类放在 :data 模块。这样其他模块只依赖接口,不依赖实现。说白了就是「面向接口编程」在依赖注入中的体现。

15.3.2 使用@EntryPoint突破隔离

有时候,某个模块需要访问另一个模块的依赖,但又不方便直接依赖。这时候可以用 @EntryPoint

举个例子::core:analytics 模块需要访问 :data 模块的 UserRepository,但 :core:analytics 是基础层,不应该依赖业务层。

// 在 :data 模块中定义 EntryPoint
@EntryPoint
@InstallIn(SingletonComponent::class)
interface DataEntryPoint {
    fun getUserRepository(): UserRepository
}

// 在 :core:analytics 模块中使用
class AnalyticsService {
    fun trackUserAction(context: Context) {
        val entryPoint = EntryPointAccessors.fromApplication(
            context.applicationContext,
            DataEntryPoint::class.java
        )
        val repo = entryPoint.getUserRepository()
        // 使用 repo
    }
}

这样,:core:analytics 模块不需要直接依赖 :data 模块,只需要知道 DataEntryPoint 接口即可。依赖关系变成了「运行时获取」,而不是「编译时依赖」。

注意:@EntryPoint 是「后门」,不要滥用。我一般只在跨模块的「基础设施」场景下使用,比如日志、分析、崩溃上报等。如果业务代码大量使用 EntryPoint,说明你的模块划分有问题。

15.4 多模块依赖管理的实战配置

下面是一个典型的多模块项目的 build.gradle.kts 配置示例:

// :app 模块
dependencies {
    implementation(project(":feature:home"))
    implementation(project(":feature:profile"))
    implementation(project(":data"))
    implementation(project(":core:network"))
    implementation(project(":core:database"))
}

// :feature:home 模块
dependencies {
    implementation(project(":data"))
    implementation(project(":core:common"))
}

// :data 模块
dependencies {
    implementation(project(":core:network"))
    implementation(project(":core:database"))
    implementation(project(":domain"))
}

// :core:network 模块
dependencies {
    // 只依赖第三方库,不依赖任何业务模块
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.11.0")
}

你看,依赖方向是单向的:app → feature → data → core。每个模块只依赖比自己更底层的模块。

15.5 模块间依赖隔离的避坑指南

我在实践中踩过不少坑,这里分享几个典型的:

  • 坑1:模块间共享同一个Hilt组件。比如两个模块都用了 @InstallIn(SingletonComponent::class),结果一个模块的依赖被另一个模块意外覆盖。解决方案:尽量使用 @InstallIn(ActivityComponent::class)@InstallIn(FragmentComponent::class) 缩小作用域。
  • 坑2:在基础层引用业务层的类型。比如 :core:network 模块的拦截器里直接引用了 :data 模块的实体类。解决方案:在基础层定义通用接口,业务层实现这些接口。
  • 坑3:模块间传递Hilt的@Module。比如在 :app 模块中直接 @Module(includes = [DataModule::class])。这样会导致模块耦合。解决方案:每个模块自己管理自己的Module,不要跨模块引用。

我曾经在一个项目里,因为模块A的Module被模块B引用,导致模块B必须依赖模块A的所有第三方库。结果模块B的APK体积直接翻了一倍。嗯,从那以后我再也不敢跨模块引用Module了。

15.6 多模块依赖管理的知识体系

下面这张图展示了多模块依赖管理的核心逻辑:

多模块依赖管理核心逻辑 应用层 (app, feature-home, feature-profile) 依赖业务层和基础层,通过Hilt获取依赖 业务层 (data, domain, repository) 提供接口和实现,通过@Module暴露依赖 基础层 (core, common, network, database) 不依赖任何业务模块,提供通用能力 核心原则:单向依赖 + 接口隔离 + 按需提供 禁止反向依赖,禁止跨模块引用Module,使用@EntryPoint作为受控后门

15.7 总结

多模块项目的依赖管理,说白了就是「划清界限、各司其职」。Hilt提供了 @Module@InstallIn@EntryPoint 等工具来帮助我们实现依赖隔离。但工具只是辅助,真正的核心在于模块划分和接口设计。

我个人建议:在项目初期就规划好模块分层,定义好接口边界。不要等到代码写了一半再来重构,那时候的成本就高得多了。

一句话总结:依赖隔离不是限制你,而是保护你。模块之间保持距离,项目才能走得更远。


公众号:蓝海资料掘金营,微信deep3321