android-clean-architecture

android-clean-architecture

熱門

适用于 Android 与 Kotlin Multiplatform (KMP) 专案的整洁架构(Clean Architecture)模式 — 包含模组结构、依赖规则、UseCase、Repository 以及资料层设计模式。

23萬星標
3.5萬分支
更新於 2026/7/14
SKILL.md
唯讀
名稱
android-clean-architecture
描述

适用于 Android 与 Kotlin Multiplatform (KMP) 专案的整洁架构(Clean Architecture)模式 — 包含模组结构、依赖规则、UseCase、Repository 以及资料层设计模式。

Android Clean Architecture

适用于 Android 与 KMP 专案的整洁架构(Clean Architecture)模式。涵盖模组边界划分、依赖反转、UseCase/Repository 模式,以及结合 Room、SQLDelight 与 Ktor 的资料层设计。

启动时机

  • 规划 Android 或 KMP 专案的模组结构时
  • 实作 UseCase、Repository 或 DataSource 时
  • 设计各层级(domain、data、presentation)之间的资料流向时
  • 使用 Koin 或 Hilt 设定依赖注入(Dependency Injection)时
  • 在分层架构中搭配 Room、SQLDelight 或 Ktor 进行开发时

模组结构

建议的架构布局

project/
├── app/                  # Android 入口点、DI 配置、Application 类别
├── core/                 # 共用工具类、基础类别、错误型态
├── domain/               # UseCase、领域模型、Repository 界面(纯 Kotlin)
├── data/                 # Repository 实作、DataSource、资料库、网络层
├── presentation/         # 页面 UI、ViewModel、UI 模型、导航
├── design-system/        # 可复用的 Compose 元件、主题、排版
└── feature/              # 功能模组(选填,适用于大型专案)
    ├── auth/
    ├── settings/
    └── profile/

依赖规则

app → presentation, domain, data, core
presentation → domain, design-system, core
data → domain, core
domain → core (or no dependencies)
core → (nothing)

关键原则domain 绝不可依赖 datapresentation 或任何框架类别,必须保持为纯粹的 Kotlin 代码。

领域层 (Domain Layer)

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,用于响应式串流
class ObserveUserProgressUseCase(
    private val repository: UserRepository
) {
    operator fun invoke(userId: String): Flow<UserProgress> {
        return repository.observeProgress(userId)
    }
}

领域模型 (Domain Models)

领域模型为纯粹的 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 Layer)

Repository 实作

负责协调本地(Local)与远程(Remote)资料源:

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 模式

将资料转换函数(Mapper)放在资料模型旁,写成扩充函式(Extension Functions):

// 在 data 层中
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 资料库 (Android)

@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)

-- 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 网络客户端 (KMP)

class ItemRemoteDataSource(private val client: HttpClient) {

    suspend fun fetchItems(category: String): List<ItemDto> {
        return client.get("api/items") {
            parameter("category", category)
        }.body()
    }
}

// 设定带有内容协商(Content Negotiation)的 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)

// Domain 模组
val domainModule = module {
    factory { GetItemsByCategoryUseCase(get()) }
    factory { ObserveUserProgressUseCase(get()) }
}

// Data 模组
val dataModule = module {
    single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
    single { ItemLocalDataSource(get()) }
    single { ItemRemoteDataSource(get()) }
}

// Presentation 模组
val presentationModule = module {
    viewModelOf(::ItemListViewModel)
    viewModelOf(::DashboardViewModel)
}

Hilt (仅限 Android)

@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 模式

使用 Result<T> 或自订的 sealed 介面传递错误:

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 Plugins)减少建置档的代码重复:

// 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")) }
    }
}

在各模组中套用:

// domain/build.gradle.kts
plugins { id("kmp-library") }

应避免的反模式 (Anti-Patterns)

  • domain 层导入 Android 框架类别 — 保持纯粹的 Kotlin 代码
  • 将资料库实体(Entity)或 DTO 直接曝露给 UI 层 — 务必转译为领域模型(Domain Models)
  • 将业务逻辑混入 ViewModel — 拆分并提取至 UseCase
  • 使用 GlobalScope 或未统筹的 Coroutines — 应使用 viewModelScope 或结构化并发(Structured Concurrency)
  • 过度臃肿的 Repository 实作 — 依职责拆分至具体的 DataSource
  • 模组循环依赖 — 若 A 依赖 B,则 B 决不能依赖 A

参考资料

参阅 Skill:compose-multiplatform-patterns 了解 UI 模式。
参阅 Skill:kotlin-coroutines-flows 了解非同步处理模式。