26、架构最佳实践:Clean Architecture、分层设计、依赖注入Hilt/Dagger、模块化
说实话,很多Android开发者做了两三年项目,代码能跑,但一遇到需求变更就头疼。我自己也经历过那种「改一个功能,崩三个页面」的至暗时刻。后来我慢慢意识到,问题不在技术,在架构。
这一章,我们就聊聊怎么把项目组织得清爽、可维护、好扩展。说白了,就是Clean Architecture、分层设计、依赖注入和模块化这四板斧。
为什么需要架构?
你想想看,一个App刚开始就几个页面,随便写写没问题。但业务一复杂,Activity里塞了几千行代码,ViewModel里全是网络请求和数据库操作……这时候你改一行代码都得提心吊胆。
架构的核心目的就两个:分离关注点和控制依赖方向。让每一层只做自己该做的事,上层依赖下层,下层绝不依赖上层。
核心原则:依赖倒置原则(Dependency Inversion Principle)——高层模块不应该依赖低层模块,两者都应该依赖抽象。
Clean Architecture 分层设计
我习惯把项目分成三层:数据层、领域层和展示层。每一层都有自己的职责,互不越界。
1. 数据层(Data Layer)
负责数据的获取和存储。包括网络请求、本地数据库、SharedPreferences等。这一层是离「脏活」最近的,但也是最容易被替换的。
- Repository:数据仓库,统一管理数据来源。我一般会写一个接口,然后实现类里决定是从网络拿还是从本地拿。
- DataSource:具体的数据源,比如Room DAO、Retrofit Service。
- DTO(Data Transfer Object):网络或数据库返回的原始数据模型,不与UI层直接耦合。
2. 领域层(Domain Layer)
这是整个架构的核心,不依赖任何Android框架。里面只放业务逻辑和用例(UseCase)。
- UseCase:一个用例就是一个业务动作。比如「获取用户信息」、「保存笔记」。每个UseCase只做一件事。
- Entity:业务实体,纯Kotlin数据类,不包含任何注解或框架依赖。
- Repository接口:定义数据操作的抽象,具体实现在数据层。
我的习惯:领域层里只放接口和纯Kotlin代码。这样单元测试特别好写,不需要启动模拟器,跑得飞快。
3. 展示层(Presentation Layer)
负责UI展示和用户交互。包括Activity、Fragment、ViewModel、Compose等。
- ViewModel:持有状态,调用UseCase。不直接操作数据库或网络。
- UI State:用密封类或数据类表示页面的各种状态(加载中、成功、失败)。
- Adapter / Composable:纯UI渲染,不包含业务逻辑。
嗯,这里要注意:展示层绝对不能直接依赖数据层的实现。ViewModel只能通过UseCase间接访问数据。
依赖注入:Hilt vs Dagger
依赖注入说白了就是「我要什么,你给我什么,我自己不去new」。我早期写代码都是手动new对象,后来项目大了,手动管理依赖简直要命。Hilt和Dagger就是来解决这个问题的。
| 特性 | Dagger | Hilt |
|---|---|---|
| 上手难度 | 高,需要手动写Component、Module | 低,注解驱动,内置Android组件支持 |
| 编译速度 | 较快 | 稍慢(因为做了更多封装) |
| 官方推荐 | 否 | 是(Google官方推荐) |
| 适用场景 | 大型项目,需要精细控制 | 大多数Android项目 |
我个人建议:新项目直接用Hilt。Dagger虽然灵活,但配置起来太啰嗦。我在一个老项目里用过Dagger,光Component就写了七八个,每次加个依赖都要改一堆文件。后来切到Hilt,清爽多了。
Hilt 基本用法
// 1. Application 类上添加 @HiltAndroidApp
@HiltAndroidApp
class MyApp : Application()
// 2. Activity 上添加 @AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModel: MainViewModel
// ...
}
// 3. ViewModel 使用 @HiltViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
private val getUserInfoUseCase: GetUserInfoUseCase
) : ViewModel() {
// ...
}
// 4. 提供依赖的 Module
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"my_database"
).build()
}
}
避坑指南:我曾经在Module里忘记加@Singleton注解,结果每次注入都是新实例,导致数据库连接泄漏。后来排查了半天才发现。所以,单例对象一定要加@Singleton。
模块化:分而治之
模块化就是把一个App拆成多个独立的模块。每个模块可以单独编译、单独测试。我参与过一个电商项目,光模块就分了十几个:common、network、database、user、product、order……
模块化的好处很明显:
- 编译速度提升:改一个模块只编译那个模块,不用全量编译。
- 团队协作:不同团队负责不同模块,互不干扰。
- 复用性:common模块可以给多个App用。
常见的模块划分方式
- 按功能划分:user模块、product模块、order模块。每个模块包含自己的数据层、领域层和展示层。
- 按层级划分::data、:domain、:presentation。每个层级一个模块,功能模块依赖这些层级模块。
- 混合模式:先按层级分,再按功能分。比如 :data:user、:data:product、:domain:user、:domain:product。
我个人比较推荐按功能划分 + 公共层级模块。比如:
app/
├── common/ // 公共工具、基类
├── data/ // 数据层公共代码
├── domain/ // 领域层公共代码
├── feature/
│ ├── user/ // 用户功能模块
│ ├── product/ // 商品功能模块
│ └── order/ // 订单功能模块
└── app/ // 主工程,组装所有模块
架构核心逻辑图
下面这张图展示了Clean Architecture在Android中的典型分层和依赖方向。注意箭头方向——依赖永远指向内层。
依赖注入与模块化的结合
模块化之后,模块之间的依赖关系怎么管理?答案就是依赖注入。Hilt可以跨模块提供依赖,只要在对应模块里定义好Module和Provides方法。
举个例子:
// 在 :data 模块中定义数据库 Module
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
}
}
// 在 :domain 模块中定义 UseCase,依赖 Repository 接口
class GetUserUseCase @Inject constructor(
private val userRepository: UserRepository
) {
suspend operator fun invoke(userId: String): User {
return userRepository.getUser(userId)
}
}
// 在 :feature:user 模块中,ViewModel 直接注入 UseCase
@HiltViewModel
class UserViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
// ...
}
小技巧:模块之间的依赖尽量用接口,不要直接依赖实现类。这样替换实现的时候,只需要改Module里的绑定,其他地方不用动。
总结一下
Clean Architecture、分层设计、依赖注入和模块化,这四样东西组合起来,就是一个能打的项目骨架。我这些年踩过的坑,十有八九都是因为架构没设计好。早期图省事,后期加倍还债。
嗯,最后说一句:架构不是银弹。小项目没必要硬套Clean Architecture,但一旦业务复杂起来,提前做好分层和模块化,绝对能让你少熬几个通宵。
核心要点回顾:
- 数据层、领域层、展示层各司其职,依赖方向由外向内
- 领域层是核心,不依赖任何框架
- Hilt简化依赖注入,新项目首选
- 模块化按功能划分,公共模块抽离
- 依赖注入是模块间解耦的关键