模块化架构设计理念
模块化这个词,大家都听过。但真正落地时,很多项目走着走着就变成了一团乱码——嗯,我自己就踩过这个坑。
2018年我接手一个电商App,代码全在一个module里,光Gradle编译就要7分钟。后来拆成30多个模块,编译时间降到90秒,Hilt注入的性能问题也更容易定位。今天我们就聊聊模块化到底该怎么拆,拆完后怎么保证依赖注入不拖后腿。
核心原则:不只是“拆开”那么简单
模块化不是把代码按文件夹分一分就完事。它是一套架构约束。我个人习惯先定三条底线:
- 高内聚,低耦合 —— 一个模块只关注一类职责,对外暴露尽量少的接口。
- 单向依赖 —— 依赖箭头永远从上层指向下层,不能回调绕圈。
- 接口隔离 —— 用抽象依赖而不是具体实现,这样Hilt才能在编译期生成精准的注入代码。
📌 关键指标:模块间依赖数量超过20条时,建议重新审视设计。我曾经在一个项目中看到两个模块互相依赖,结果Hilt编译时产生了大量代理类,包体积暴涨3MB。
分层架构:从顶到底的5层模型
大多数成熟项目会采用“表现层 → 业务层 → 数据层 → 网络/存储层 → 基础层”的层次。每一层只依赖它的下一层,通过接口解耦。下面这个表格可以帮助你快速对标:
| 层次 | 典型模块名 | 职责 | 依赖方向 |
|---|---|---|---|
| 表现层 | app, :feature:home | UI、导航、ViewModel | → 业务层 |
| 业务层 | :business:order | 用例、领域模型 | → 数据层 |
| 数据层 | :data:impl | 仓库实现、DTO转换 | → 网络/存储层 |
| 网络/存储层 | :network, :storage | API调用、数据库操作 | → 基础层 |
| 基础层 | :core, :common | 工具类、Hilt公共组件 | 无(最底层) |
💡 小技巧:基础层里放Hilt的@Module定义(比如OkHttp、Gson的绑定),上层模块不需要重复声明。这样编译期扫描的Module数减少,注入速度自然更快。
依赖关系管理:避免“蜘蛛网”
模块多了,依赖关系容易乱。我一般强制团队遵守两条规则:
- 禁止模块间循环依赖 —— Gradle会报错,但有些人会用“接口+动态查找”绕过检查,这会导致Hilt在运行时找不到Provider。曾经有个线上bug就是因为这种伪循环。
- 跨模块调用一律走接口 —— 比如业务层需要调用数据层,就在数据层定义一个接口,放在:data:api模块,实现放在:data:impl。Hilt的@Binds正好解决这种绑定。
// 数据层接口模块 :data:api
interface OrderRepository {
fun getOrders(): Flow<List<Order>>
}
// 数据层实现模块 :data:impl
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
abstract fun bindOrderRepository(impl: OrderRepositoryImpl): OrderRepository
}
这样一拆,Hilt在编译期就知道哪个实现对应哪个接口,不需要运行时反射,性能影响几乎为零。
模块化对Hilt注入性能的影响
为什么很多团队拆了模块后体验反而变差?因为你没管好“Component树”。
⚠️ 常见陷阱:每个Feature模块都创建一个独立Component,导致Hilt生成大量子Component,每次注入都要遍历整个树。我实测过,30个模块每个带一个子Component,Application初始化时间增加400ms。
正确做法是:让Feature模块共用SingletonComponent,只在需要隔离作用域时(如用户登录/未登录)才创建子Component。Hilt的@ScopedFeature本质上就是用@Component.Factory实现的,但一定要评估必要性。
// 推荐:大部分模块直接使用SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object SharedModule {
@Provides
fun provideCache(): Cache = ...
}
// 只有明确需要不同生命周期的模块才新建Component
@Module
@InstallIn(SessionComponent::class)
object SessionModule { ... }
另外,注意@AndroidEntryPoint的使用范围——它会给每个Activity/Fragment生成一个Hilt_Fragment基类。如果你的模块里有大量Fragment,编译期会膨胀。我建议只在需要DI的页面使用,纯工具Fragment用传统方式即可。
避坑指南
- 避免“上帝模块” —— 我曾见过一个:core模块包含了网络、数据库、工具类、自定义View,结果Hilt的Scope全混在一起,谁都能注入谁,导致循环依赖排查了三天。
- 小心跨模块的@Provides方法过多 —— 每个@Provides都会在编译期生成Binding类。如果:core模块提供了100+个绑定,编译速度会直线下降。可以考虑按功能拆成多个@Module。
- 模块粒度和Gradle缓存 —— 模块拆得太细(比如一个类一个模块)会让Gradle的增量编译优势完全消失。一般以功能边界为粒度,单模块代码量控制在5000行以内。
模块化架构与Hilt注入的关系图
下面这张图展示了我推荐的模块分层及依赖方向,其中基础层的Hilt公共Module被所有上层模块共享,但上层不直接依赖基础层的内部实现。箭头代表“依赖关系”,虚线代表“通过接口绑定实现”。
总结一下
模块化架构的核心就三个字:解耦。但解耦一定要考虑Hilt的编译特性——过多的Component和过大的Module都会拖慢构建。我个人推荐按“业务边界”来拆,每层只依赖下一层的接口,公共依赖统一放在基础层。这样你既获得了模块化的灵活性,又不会牺牲注入性能。
记住一句话:模块化的最终目的是让“变”变得安全且快速。Hilt只是帮你管理“变”的工具,别让工具反过来限制了架构。