用分层把复杂度关进笼子——Android Clean Architecture 实践心法
很多团队接触 Clean Architecture 的契机是「代码乱了,要重构」。但我观察下来,真正让这套架构发挥价值的,不是那些层的名字,而是一条强制方向的依赖规则——以及你愿不愿意认真执行它。
核心主张:依赖方向是架构的心脏
Clean Architecture 的模块依赖关系只有一条线方向:
app → presentation → domain ← data
↑
core
domain 层是整个应用最纯粹的部分——纯 Kotlin,没有任何 Android 框架导入,没有 Room 注解,没有 Retrofit 接口,只有业务模型和 Repository 的接口定义。data 层实现这些接口,presentation 层消费 UseCase。
这条规则最容易被破坏的地方是:Repository 接口放错了位置。我见过不少项目把 ItemRepository 定义在 data 包里,然后 presentation 去依赖 data——两层直接耦合,domain 形同虚设。正确做法是接口定义在 domain,实现在 data,这是依赖倒置的核心。
domain 模块的 build.gradle.kts 里永远不应该出现 Android 或第三方框架依赖。如果你发现 domain 在 import android.* 或 retrofit2.*,架构已经坏了。
UseCase:一个类,一件事
UseCase 是 domain 层的动词。每个 UseCase 只做一件业务上的事,用 operator fun invoke 让调用侧写起来像函数调用:
class GetItemsByCategoryUseCase(
private val repository: ItemRepository
) {
suspend operator fun invoke(category: String): Result<List<Item>> {
return repository.getItemsByCategory(category)
}
}
// ViewModel 侧
val result = getItemsByCategory("books")这样写有个不明显的好处:ViewModel 测试时只需要 mock UseCase,不用 mock 整条 Repository → DataSource 链。UseCase 本身也容易单独测,因为它依赖的是接口而不是实现。
对于需要持续观测的数据,用 Flow-based UseCase:返回 Flow<T> 而非 suspend 函数,让 ViewModel 收集数据流而不是主动拉取。
Repository 的真实职责:协调,不是堆逻辑
Repository 实现层(在 data 包)做的是「协调本地数据源与远程数据源」,而不是堆业务判断。典型结构:
class ItemRepositoryImpl(
private val localDataSource: ItemLocalDataSource,
private val remoteDataSource: ItemRemoteDataSource
) : ItemRepository {
override suspend fun getItemsByCategory(category: String): Result<List<Item>> {
return runCatching {
val remote = remoteDataSource.fetchItems(category)
localDataSource.insertItems(remote.map { it.toEntity() })
localDataSource.getItemsByCategory(category).map { it.toDomain() }
}
}
}注意 mapper 的位置:toDomain() 和 toEntity() 是 extension function,定义在数据层靠近各自模型的文件里。这让 domain model 保持干净——它只是一个朴素的 data class,不带任何 ORM 注解。
「Fat Repository」是最常见的反模式:把过滤逻辑、缓存策略、业务判断全堆在 Repository 里,几百行后没人敢动。解法是把 Repository 拆成多个聚焦的 DataSource,Repository 只做胶水。
KMP 场景下多了什么
Kotlin Multiplatform 项目里,domain 层几乎不用改——本来就是纯 Kotlin。变的是 data 层:
- Room 换成 SQLDelight(跨平台 SQL 方案,
.sq文件编译期生成类型安全查询) - Retrofit 换成 Ktor(跨平台 HTTP 客户端)
- DI 用 Koin 而不是 Hilt(Hilt 强绑 Android)
这个替换过程让我对「domain 层不依赖框架」的价值有了更直观的感受——真正需要改的只有 data 层,domain 和 presentation 的大部分代码可以直接共享。
常见反模式备忘
几个让架构悄悄腐化的习惯:
- ViewModel 里写业务逻辑:应该提取成 UseCase,ViewModel 只负责状态管理和 UI 事件转换
- 把 Entity/DTO 直接传给 UI:Room 的
@Entity类不该出现在presentation层,永远映射成 domain model 再往上传 - 用
GlobalScope启动协程:任何异步操作都应该在viewModelScope或受控的 coroutine scope 里,生命周期之外的协程是内存泄漏的温床 - 模块循环依赖:Gradle 会直接报错,但如果你在
domain里用了某个data的类型,就已经形成了概念上的循环
一句话心法
依赖方向不是约定,是规则——让
domain对任何框架一无所知,架构的稳定性才真正有保障。