关于 android-data-layer
面向 Android 开发者的数据层技能,介绍仓库、错误传播与 Room 持久化的组织方式。说明平台异常应在哪一层转换,以及如何为 Kotlin Multiplatform 配置 Room。
主要功能
- 01仓库边界设计
- 02分层错误映射
- 03按架构层处理 Result
- 04KMP Room 配置指导
- 05按目标配置 KSP
适用场景
- 01避免平台异常传到 ViewModel
- 02确定仓库与用例的错误边界
- 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())) }
}
存在领域层时,边界向外移动一层:
- 数据源抛出平台/库异常(
IOException、HttpException、SQLiteException)。 - 存储库是错误边界——捕获这些异常并映射为
DataError;绝不让原始平台类型泄露。如果没有领域层(简单 MVVM): 存储库直接返回Result<T>,由 ViewModel 处理,且无需了解平台异常。 - 用例(如果存在)是
Result边界——存储库改为抛出DataError,由用例捕获并返回带有领域特定错误模型的Result<T>。用例绝不捕获平台异常类型。 - ViewModel 处理
Result<T>并将其映射为 UI 状态(明确的加载/成功/错误)。
(Room 类型默认使用 Entity 后缀——ArticleEntity;如果项目已有约定,例如使用 Cached 前缀,则保持一致。)
KMP 中的 Room(commonMain)
自 2.7.0 起,Room 已在 KMP 中稳定。共享配置与仅适用于 Android 的配置有三处不同:
- 在
@Database上使用@ConstructedBy(...),并配套使用 Room 会为其生成各平台actual的expect object。 - 使用
BundledSQLiteDriver(来自androidx.sqlite:sqlite-bundled)——确保 Android/iOS/JVM/Web 使用相同的 SQLite 版本(Android 的系统 SQLite 版本会因 API 级别和设备而异)。 - 使用
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 配置。