mcpField
A
android-data-layer

android-data-layer

github.com/rcosteira79开发169152 次安装6 周前更新
代码仓库

关于 android-data-layer

面向 Android 开发者的数据层技能,介绍仓库、错误传播与 Room 持久化的组织方式。说明平台异常应在哪一层转换,以及如何为 Kotlin Multiplatform 配置 Room。

主要功能

  1. 01仓库边界设计
  2. 02分层错误映射
  3. 03按架构层处理 Result
  4. 04KMP Room 配置指导
  5. 05按目标配置 KSP

适用场景

  1. 01避免平台异常传到 ViewModel
  2. 02确定仓库与用例的错误边界
  3. 03在 KMP 中配置共享 Room 数据库

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rcosteira79/android-skills --skill android-data-layer

安装后在 Claude Code 中输入/android-data-layer确认已加载。

详细介绍

Android 数据层

数据层负责协调来自多个来源的数据;其公共 API 是存储库接口,内部实现(DAO、API 服务、DTO)绝不会向上层泄露。本技能是错误传播模型和 KMP-Room 配置的权威参考。相关: android-skills:android-retrofit(Retrofit 服务/OkHttp/Hilt 接线)、android-skills:android-dev(整体应用架构)。

错误传播——分层模型

目标:数据层错误(IOException/HttpException/SQLiteException)绝不能传到 ViewModel。在存储库边界将它们映射为领域错误类型。如果项目已有错误约定,请保持一致;否则,密封的 DataError 层次结构是合理的默认选择:

sealed class DataError(message: String, cause: Throwable? = null) : Exception(message, cause) {
    class Network(cause: Throwable) : DataError("Network error", cause)
    class Server(val code: Int, message: String?) : DataError("Server error $code: $message")
    class Local(cause: Throwable) : DataError("Local storage error", cause)
}

// repository = error boundary: catch platform exceptions, return Result<T> (no domain layer) or throw DataError (with one)
suspend fun refreshNews(): Result<Unit> = withContext(io) {
    try { newsDao.insertAll(newsApi.fetchLatest().map(NewsDto::toEntity)); Result.success(Unit) }
    catch (e: IOException) { Result.failure(DataError.Network(e)) }
    catch (e: HttpException) { Result.failure(DataError.Server(e.code(), e.message())) }
}

存在领域层时,边界向外移动一层:

  1. 数据源抛出平台/库异常(IOException、HttpException、SQLiteException)。
  2. 存储库是错误边界——捕获这些异常并映射为 DataError;绝不让原始平台类型泄露。如果没有领域层(简单 MVVM): 存储库直接返回 Result<T>,由 ViewModel 处理,且无需了解平台异常。
  3. 用例(如果存在)是 Result 边界——存储库改为抛出 DataError,由用例捕获并返回带有领域特定错误模型的 Result<T>。用例绝不捕获平台异常类型。
  4. ViewModel 处理 Result<T> 并将其映射为 UI 状态(明确的加载/成功/错误)。

(Room 类型默认使用 Entity 后缀——ArticleEntity;如果项目已有约定,例如使用 Cached 前缀,则保持一致。)

KMP 中的 Room(commonMain)

自 2.7.0 起,Room 已在 KMP 中稳定。共享配置与仅适用于 Android 的配置有三处不同:

  1. 在 @Database 上使用 @ConstructedBy(...),并配套使用 Room 会为其生成各平台 actual 的 expect object。
  2. 使用 BundledSQLiteDriver(来自 androidx.sqlite:sqlite-bundled)——确保 Android/iOS/JVM/Web 使用相同的 SQLite 版本(Android 的系统 SQLite 版本会因 API 级别和设备而异)。
  3. 使用 setQueryCoroutineContext(Dispatchers.IO)——Android 上的 Room 默认如此;KMP 则不会。
// commonMain
@Database(entities = [ArticleEntity::class], version = 1, exportSchema = true)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() { abstract fun articleDao(): ArticleDao }

@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase>

fun getRoomDatabase(builder: RoomDatabase.Builder<AppDatabase>): AppDatabase =
    builder.setDriver(BundledSQLiteDriver()).setQueryCoroutineContext(Dispatchers.IO).build()

每个平台都提供自己的构建器——Android 使用 Room.databaseBuilder(context, AppDatabase::class.java, "app.db");iOS/JVM 使用 Room.databaseBuilder<AppDatabase>(databasePath = …)。必须按目标平台配置 KSP(ksp(libs.androidx.room.compiler) 仅适用于 Android):

dependencies {
    add("kspAndroid", libs.androidx.room.compiler)
    add("kspIosArm64", libs.androidx.room.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room.compiler)
    // … one per target
}
room { schemaDirectory("$projectDir/schemas") }   // export the schema for migration-history tracking

对于纯 Android 项目,可跳过 @ConstructedBy,直接调用 Room.databaseBuilder(context, AppDatabase::class.java, "app.db")——Room 的行为完全相同;只有在数据层必须位于 commonMain 时,才选择启用 KMP 配置。

评论

支持 Markdown