android-clean-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAndroid Clean Architecture
Android Clean Architecture
Clean Architecture patterns for Android and KMP projects. Covers module boundaries, dependency inversion, UseCase/Repository patterns, and data layer design with Room, SQLDelight, and Ktor.
适用于Android和KMP项目的Clean Architecture模式。涵盖模块边界、依赖倒置、UseCase/Repository模式,以及结合Room、SQLDelight和Ktor的数据层设计。
When to Activate
适用场景
- Structuring Android or KMP project modules
- Implementing UseCases, Repositories, or DataSources
- Designing data flow between layers (domain, data, presentation)
- Setting up dependency injection with Koin or Hilt
- Working with Room, SQLDelight, or Ktor in a layered architecture
- 构建Android或KMP项目的模块结构
- 实现UseCase、Repository或DataSource
- 设计领域层、数据层、表现层之间的数据流
- 使用Koin或Hilt配置依赖注入
- 在分层架构中使用Room、SQLDelight或Ktor
Module Structure
模块结构
Recommended Layout
推荐布局
project/
├── app/ # Android entry point, DI wiring, Application class
├── core/ # Shared utilities, base classes, error types
├── domain/ # UseCases, domain models, repository interfaces (pure Kotlin)
├── data/ # Repository implementations, DataSources, DB, network
├── presentation/ # Screens, ViewModels, UI models, navigation
├── design-system/ # Reusable Compose components, theme, typography
└── feature/ # Feature modules (optional, for larger projects)
├── auth/
├── settings/
└── profile/project/
├── app/ # Android入口点、依赖注入配置、Application类
├── core/ # 共享工具类、基础类、错误类型
├── domain/ # UseCase、领域模型、Repository接口(纯Kotlin实现)
├── data/ # Repository实现、DataSource、数据库、网络层
├── presentation/ # 界面、ViewModel、UI模型、导航
├── design-system/ # 可复用Compose组件、主题、排版
└── feature/ # 功能模块(可选,适用于大型项目)
├── auth/
├── settings/
└── profile/Dependency Rules
依赖规则
app → presentation, domain, data, core
presentation → domain, design-system, core
data → domain, core
domain → core (or no dependencies)
core → (nothing)Critical: must NEVER depend on , , or any framework. It contains pure Kotlin only.
domaindatapresentationapp → presentation, domain, data, core
presentation → domain, design-system, core
data → domain, core
domain → core(或无依赖)
core → (无依赖)关键注意点:层绝对不能依赖、或任何框架。它只能包含纯Kotlin代码。
domaindatapresentationDomain Layer
领域层
UseCase Pattern
UseCase模式
Each UseCase represents one business operation. Use for clean call sites:
operator fun invokekotlin
class GetItemsByCategoryUseCase(
private val repository: ItemRepository
) {
suspend operator fun invoke(category: String): Result<List<Item>> {
return repository.getItemsByCategory(category)
}
}
// Flow-based UseCase for reactive streams
class ObserveUserProgressUseCase(
private val repository: UserRepository
) {
operator fun invoke(userId: String): Flow<UserProgress> {
return repository.observeProgress(userId)
}
}每个UseCase代表一个业务操作。使用实现简洁的调用方式:
operator fun invokekotlin
class GetItemsByCategoryUseCase(
private val repository: ItemRepository
) {
suspend operator fun invoke(category: String): Result<List<Item>> {
return repository.getItemsByCategory(category)
}
}
// 基于Flow的UseCase,用于响应式流
class ObserveUserProgressUseCase(
private val repository: UserRepository
) {
operator fun invoke(userId: String): Flow<UserProgress> {
return repository.observeProgress(userId)
}
}Domain Models
领域模型
Domain models are plain Kotlin data classes — no framework annotations:
kotlin
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 }领域模型是纯Kotlin数据类——不包含任何框架注解:
kotlin
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 Interfaces
Repository接口
Defined in domain, implemented in data:
kotlin
interface ItemRepository {
suspend fun getItemsByCategory(category: String): Result<List<Item>>
suspend fun saveItem(item: Item): Result<Unit>
fun observeItems(): Flow<List<Item>>
}在领域层定义,在数据层实现:
kotlin
interface ItemRepository {
suspend fun getItemsByCategory(category: String): Result<List<Item>>
suspend fun saveItem(item: Item): Result<Unit>
fun observeItems(): Flow<List<Item>>
}Data Layer
数据层
Repository Implementation
Repository实现
Coordinates between local and remote data sources:
kotlin
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() }
}
}
override suspend fun saveItem(item: Item): Result<Unit> {
return runCatching {
localDataSource.insertItems(listOf(item.toEntity()))
}
}
override fun observeItems(): Flow<List<Item>> {
return localDataSource.observeAll().map { entities ->
entities.map { it.toDomain() }
}
}
}协调本地和远程数据源:
kotlin
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() }
}
}
override suspend fun saveItem(item: Item): Result<Unit> {
return runCatching {
localDataSource.insertItems(listOf(item.toEntity()))
}
}
override fun observeItems(): Flow<List<Item>> {
return localDataSource.observeAll().map { entities ->
entities.map { it.toDomain() }
}
}
}Mapper Pattern
映射器模式
Keep mappers as extension functions near the data models:
kotlin
// In data layer
fun ItemEntity.toDomain() = Item(
id = id,
title = title,
description = description,
tags = tags.split("|"),
status = Status.valueOf(status),
category = category
)
fun ItemDto.toEntity() = ItemEntity(
id = id,
title = title,
description = description,
tags = tags.joinToString("|"),
status = status,
category = category
)将映射器作为扩展函数放在数据模型附近:
kotlin
// 在数据层
fun ItemEntity.toDomain() = Item(
id = id,
title = title,
description = description,
tags = tags.split("|"),
status = Status.valueOf(status),
category = category
)
fun ItemDto.toEntity() = ItemEntity(
id = id,
title = title,
description = description,
tags = tags.joinToString("|"),
status = status,
category = category
)Room Database (Android)
Room数据库(Android)
kotlin
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey val id: String,
val title: String,
val description: String,
val tags: String,
val status: String,
val category: String
)
@Dao
interface ItemDao {
@Query("SELECT * FROM items WHERE category = :category")
suspend fun getByCategory(category: String): List<ItemEntity>
@Upsert
suspend fun upsert(items: List<ItemEntity>)
@Query("SELECT * FROM items")
fun observeAll(): Flow<List<ItemEntity>>
}kotlin
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey val id: String,
val title: String,
val description: String,
val tags: String,
val status: String,
val category: String
)
@Dao
interface ItemDao {
@Query("SELECT * FROM items WHERE category = :category")
suspend fun getByCategory(category: String): List<ItemEntity>
@Upsert
suspend fun upsert(items: List<ItemEntity>)
@Query("SELECT * FROM items")
fun observeAll(): Flow<List<ItemEntity>>
}SQLDelight (KMP)
SQLDelight(KMP)
sql
-- Item.sq
CREATE TABLE ItemEntity (
id TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL,
tags TEXT NOT NULL,
status TEXT NOT NULL,
category TEXT NOT NULL
);
getByCategory:
SELECT * FROM ItemEntity WHERE category = ?;
upsert:
INSERT OR REPLACE INTO ItemEntity (id, title, description, tags, status, category)
VALUES (?, ?, ?, ?, ?, ?);
observeAll:
SELECT * FROM ItemEntity;sql
-- Item.sq
CREATE TABLE ItemEntity (
id TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL,
tags TEXT NOT NULL,
status TEXT NOT NULL,
category TEXT NOT NULL
);
getByCategory:
SELECT * FROM ItemEntity WHERE category = ?;
upsert:
INSERT OR REPLACE INTO ItemEntity (id, title, description, tags, status, category)
VALUES (?, ?, ?, ?, ?, ?);
observeAll:
SELECT * FROM ItemEntity;Ktor Network Client (KMP)
Ktor网络客户端(KMP)
kotlin
class ItemRemoteDataSource(private val client: HttpClient) {
suspend fun fetchItems(category: String): List<ItemDto> {
return client.get("api/items") {
parameter("category", category)
}.body()
}
}
// HttpClient setup with content negotiation
val httpClient = HttpClient {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
install(Logging) { level = LogLevel.HEADERS }
defaultRequest { url("https://api.example.com/") }
}kotlin
class ItemRemoteDataSource(private val client: HttpClient) {
suspend fun fetchItems(category: String): List<ItemDto> {
return client.get("api/items") {
parameter("category", category)
}.body()
}
}
// 配置带内容协商的HttpClient
val httpClient = HttpClient {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
install(Logging) { level = LogLevel.HEADERS }
defaultRequest { url("https://api.example.com/") }
}Dependency Injection
依赖注入
Koin (KMP-friendly)
Koin(支持KMP)
kotlin
// Domain module
val domainModule = module {
factory { GetItemsByCategoryUseCase(get()) }
factory { ObserveUserProgressUseCase(get()) }
}
// Data module
val dataModule = module {
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
single { ItemLocalDataSource(get()) }
single { ItemRemoteDataSource(get()) }
}
// Presentation module
val presentationModule = module {
viewModelOf(::ItemListViewModel)
viewModelOf(::DashboardViewModel)
}kotlin
// 领域层模块
val domainModule = module {
factory { GetItemsByCategoryUseCase(get()) }
factory { ObserveUserProgressUseCase(get()) }
}
// 数据层模块
val dataModule = module {
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
single { ItemLocalDataSource(get()) }
single { ItemRemoteDataSource(get()) }
}
// 表现层模块
val presentationModule = module {
viewModelOf(::ItemListViewModel)
viewModelOf(::DashboardViewModel)
}Hilt (Android-only)
Hilt(仅Android)
kotlin
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
}
@HiltViewModel
class ItemListViewModel @Inject constructor(
private val getItems: GetItemsByCategoryUseCase
) : ViewModel()kotlin
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
}
@HiltViewModel
class ItemListViewModel @Inject constructor(
private val getItems: GetItemsByCategoryUseCase
) : ViewModel()Error Handling
错误处理
Result/Try Pattern
Result/Try模式
Use or a custom sealed type for error propagation:
Result<T>kotlin
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
}
// In ViewModel — map to UI state
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) }
}
}使用或自定义密封类进行错误传播:
Result<T>kotlin
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) }
}
}Convention Plugins (Gradle)
约定插件(Gradle)
For KMP projects, use convention plugins to reduce build file duplication:
kotlin
// build-logic/src/main/kotlin/kmp-library.gradle.kts
plugins {
id("org.jetbrains.kotlin.multiplatform")
}
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
sourceSets {
commonMain.dependencies { /* shared deps */ }
commonTest.dependencies { implementation(kotlin("test")) }
}
}Apply in modules:
kotlin
// domain/build.gradle.kts
plugins { id("kmp-library") }对于KMP项目,使用约定插件减少构建文件重复:
kotlin
// build-logic/src/main/kotlin/kmp-library.gradle.kts
plugins {
id("org.jetbrains.kotlin.multiplatform")
}
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
sourceSets {
commonMain.dependencies { /* 共享依赖 */ }
commonTest.dependencies { implementation(kotlin("test")) }
}
}在模块中应用:
kotlin
// domain/build.gradle.kts
plugins { id("kmp-library") }Anti-Patterns to Avoid
需要避免的反模式
- Importing Android framework classes in — keep it pure Kotlin
domain - Exposing database entities or DTOs to the UI layer — always map to domain models
- Putting business logic in ViewModels — extract to UseCases
- Using or unstructured coroutines — use
GlobalScopeor structured concurrencyviewModelScope - Fat repository implementations — split into focused DataSources
- Circular module dependencies — if A depends on B, B must not depend on A
- 在层导入Android框架类——保持纯Kotlin实现
domain - 向UI层暴露数据库实体或DTO——始终映射为领域模型
- 在ViewModel中编写业务逻辑——提取到UseCase中
- 使用或非结构化协程——使用
GlobalScope或结构化并发viewModelScope - 臃肿的Repository实现——拆分为专注的DataSource
- 循环模块依赖——如果A依赖B,B绝对不能依赖A
References
参考资料
See skill: for UI patterns.
See skill: for async patterns.
compose-multiplatform-patternskotlin-coroutines-flows查看技能:获取UI模式。
查看技能:获取异步模式。
compose-multiplatform-patternskotlin-coroutines-flows