返回博客

用分层把复杂度关进笼子——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 层,domainpresentation 的大部分代码可以直接共享。

常见反模式备忘

几个让架构悄悄腐化的习惯:

  • ViewModel 里写业务逻辑:应该提取成 UseCase,ViewModel 只负责状态管理和 UI 事件转换
  • 把 Entity/DTO 直接传给 UI:Room 的 @Entity 类不该出现在 presentation 层,永远映射成 domain model 再往上传
  • GlobalScope 启动协程:任何异步操作都应该在 viewModelScope 或受控的 coroutine scope 里,生命周期之外的协程是内存泄漏的温床
  • 模块循环依赖:Gradle 会直接报错,但如果你在 domain 里用了某个 data 的类型,就已经形成了概念上的循环

一句话心法

依赖方向不是约定,是规则——让 domain 对任何框架一无所知,架构的稳定性才真正有保障。