android-clean-architecture
面向 Android 与 Kotlin Multiplatform 的 Clean Architecture 实现指南。涵盖模块分层结构、依赖方向规则、UseCase / Repository / DataSource 三层数据模式,以及 Room / SQLDelight / Ktor 的数据层选型。同时提供 Koin 和 Hilt 两种依赖注入方案与结构化错误处理。
核心能力
Android & KMP 项目的 Clean Architecture 完整实践
为 Android 和 Kotlin Multiplatform 项目提供模块化的分层架构方案,明确 domain、data、presentation 等各层职责与依赖边界。通过 UseCase 封装业务操作、Repository 协调本地与远程数据源、DataSource 隔离具体技术实现,实现可测试、可替换、可演进的项目结构。
适用场景
- 搭建 Android 或 KMP 项目的模块结构
- 实现 UseCase、Repository、DataSource 三层数据流
- 设计 domain / data / presentation 之间的数据流转
- 配置 Koin 或 Hilt 依赖注入
- 集成 Room(Android)、SQLDelight(KMP)、Ktor(网络层)等技术栈
推荐模块结构
project/
├── app/ # Android 入口,DI 装配,Application 类
├── core/ # 共享工具、基类、错误类型
├── domain/ # UseCase、领域模型、Repository 接口(纯 Kotlin)
├── data/ # Repository 实现、DataSource、数据库、网络
├── presentation/ # 页面、ViewModel、UI 模型、导航
├── design-system/ # 可复用 Compose 组件、主题、排版
└── feature/ # 特性模块(可选,大型项目使用)
├── auth/
├── settings/
└── profile/
依赖规则(关键约束)
app → presentation, domain, data, core
presentation → domain, design-system, core
data → domain, core
domain → core(或零依赖)
core → (无依赖)
铁律:domain 层绝对不可依赖 data、presentation 或任何框架,必须为纯 Kotlin。
Domain 层设计
UseCase 模式
每个 UseCase 代表一个业务操作,使用 operator fun invoke 实现简洁的调用语法:
class GetItemsByCategoryUseCase(
private val repository: ItemRepository
) {
suspend operator fun invoke(category: String): Result<List<Item>> {
return repository.getItemsByCategory(category)
}
}
也支持基于 Flow 的响应式 UseCase,用于实时数据流。
领域模型
纯 Kotlin data class,无任何框架注解:
data class Item(
val id: String,
val title: String,
val description: String,
val tags: List<String>,
val status: Status,
val category: String
)
enum class Status { DRAFT, ACTIVE, ARCHIVED }
Repository 接口
接口定义在 domain 层,具体实现放在 data 层:
interface ItemRepository {
suspend fun getItemsByCategory(category: String): Result<List<Item>>
suspend fun saveItem(item: Item): Result<Unit>
fun observeItems(): Flow<List<Item>>
}
Data 层实现
Repository 实现
协调本地与远程数据源,封装数据同步逻辑:
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 模式
将映射逻辑以扩展函数形式放在数据模型附近:
fun ItemEntity.toDomain() = Item(...)
fun ItemDto.toEntity() = ItemEntity(...)
技术选型
| 技术 | 平台 | 用途 |
|---|---|---|
| Room | Android | 本地 SQLite 数据库,Entity + Dao |
| SQLDelight | KMP | 跨平台类型安全 SQL,生成 Kotlin API |
| Ktor | KMP | HTTP 客户端,支持内容协商、日志、默认请求配置 |
依赖注入
Koin(KMP 兼容)
跨平台首选,模块定义简洁:
val domainModule = module {
factory { GetItemsByCategoryUseCase(get()) }
}
val dataModule = module {
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
}
Hilt(仅限 Android)
基于注解,适合纯 Android 项目:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
}
错误处理
Result / Try 模式
使用 Result<T> 或自定义密封类型进行错误传播:
sealed interface Try<out T> {
data class Success<T>(val value: T) : Try<T>
data class Failure(val error: AppError) : Try<Nothing>
}
sealed interface AppError {
data class Network(val message: String) : AppError
data class Database(val message: String) : AppError
data object Unauthorized : AppError
}
在 ViewModel 中将结果映射为 UI 状态:
viewModelScope.launch {
when (val result = getItems(category)) {
is Try.Success -> _state.update { it.copy(items = result.value, isLoading = false) }
is Try.Failure -> _state.update { it.copy(error = result.error.toMessage(), isLoading = false) }
}
}
Gradle Convention Plugins
KMP 项目使用 Convention Plugin 减少构建脚本重复:
// build-logic/src/main/kotlin/kmp-library.gradle.kts
plugins {
id("org.jetbrains.kotlin.multiplatform")
}
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
}
模块直接应用:
// domain/build.gradle.kts
plugins { id("kmp-library") }
反模式(必须避免)
- 在
domain层引入 Android 框架类 — 必须保持纯 Kotlin - 将数据库实体或 DTO 暴露给 UI 层 — 始终映射为领域模型
- 在 ViewModel 中编写业务逻辑 — 提取到 UseCase
- 使用
GlobalScope或无结构协程 — 使用viewModelScope或结构化并发 - 臃肿的 Repository — 拆分为聚焦的 DataSource
- 循环模块依赖 — A 依赖 B,则 B 绝不可依赖 A