Android Clean Architecture

affaan-m/ECC/skills/android-clean-architecture

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775無授權條款275K 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫4 天前更新

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

AI 產生的概覽

為 Android 與 Kotlin Multiplatform 專案提供整潔架構的分層、UseCase、Repository 與資料來源組織指南。

功能
此技能提供將 Android 與 Kotlin Multiplatform 程式碼庫劃分為 app、core、domain、data、presentation 與 feature 模組的參考模式,並給出明確的相依性規則。它記錄了 UseCase 與 Repository 模式、映射函式、Room 與 SQLDelight 持久化、Ktor 網路請求、Koin 與 Hilt 相依性注入、使用 Result 或密封型別的錯誤處理,以及 Gradle 慣例外掛。它也列出應避免的反模式,例如在 domain 層引入框架類別或在 ViewModel 中放置業務邏輯。
適用情境
適用於規劃 Android 或 KMP 專案的模組與分層,或實作 UseCase、Repository、DataSource 與相依性注入時。也適合設計 domain、data 與 presentation 層之間的資料流,或在分層架構中使用 Room、SQLDelight、Ktor 時參考。
執行需求
除代理外不需任何指令碼或工具,它是純說明性參考。其描述的模式面向可能使用 Room、SQLDelight、Ktor、Koin、Hilt 與 Gradle 慣例外掛的 Kotlin 專案,但閱讀本身無需安裝任何內容。

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.

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

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/

Dependency Rules

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

Critical: domain must NEVER depend on data, presentation, or any framework. It contains pure Kotlin only.

Domain Layer

UseCase Pattern

Each UseCase represents one business operation. Use operator fun invoke for clean call sites:

kotlin
class GetItemsByCategoryUseCase(    private val repository: ItemRepository) {    suspend operator fun invoke(category: String): Result<List<Item>> {        return repository.getItemsByCategory(category)    }}
// Flow-based UseCase for reactive streamsclass 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 }

Repository Interfaces

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>>}

Data Layer

Repository Implementation

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() }        }    }}

Mapper Pattern

Keep mappers as extension functions near the data models:

kotlin
// In data layerfun 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)

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

sql
-- Item.sqCREATE 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)

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 negotiationval httpClient = HttpClient {    install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }    install(Logging) { level = LogLevel.HEADERS }    defaultRequest { url("https://api.example.com/") }}

Dependency Injection

Koin (KMP-friendly)

kotlin
// Domain moduleval domainModule = module {    factory { GetItemsByCategoryUseCase(get()) }    factory { ObserveUserProgressUseCase(get()) }}
// Data moduleval dataModule = module {    single<ItemRepository> { ItemRepositoryImpl(get(), get()) }    single { ItemLocalDataSource(get()) }    single { ItemRemoteDataSource(get()) }}
// Presentation moduleval presentationModule = module {    viewModelOf(::ItemListViewModel)    viewModelOf(::DashboardViewModel)}

Hilt (Android-only)

kotlin
@Module@InstallIn(SingletonComponent::class)abstract class RepositoryModule {    @Binds    abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository}
@HiltViewModelclass ItemListViewModel @Inject constructor(    private val getItems: GetItemsByCategoryUseCase) : ViewModel()

Error Handling

Result/Try Pattern

Use Result<T> or a custom sealed type for error propagation:

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 stateviewModelScope.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)

For KMP projects, use convention plugins to reduce build file duplication:

kotlin
// build-logic/src/main/kotlin/kmp-library.gradle.ktsplugins {    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.ktsplugins { id("kmp-library") }

Anti-Patterns to Avoid

  • Importing Android framework classes in domain — keep it pure Kotlin
  • Exposing database entities or DTOs to the UI layer — always map to domain models
  • Putting business logic in ViewModels — extract to UseCases
  • Using GlobalScope or unstructured coroutines — use viewModelScope or structured concurrency
  • Fat repository implementations — split into focused DataSources
  • Circular module dependencies — if A depends on B, B must not depend on A

References

See skill: compose-multiplatform-patterns for UI patterns. See skill: kotlin-coroutines-flows for async patterns.

來源與署名

來源:affaan-m/ECC位於skills/android-clean-architecture提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架