Data 层与 Room 设计
约 735 字大约 2 分钟
2026-03-20
1. 背景与目标
这一篇只讲一件事:Data 层如何把“课表业务”落地成可持续维护的本地数据库结构。
- 新手常见问题:知道要存数据,但不知道表怎么拆、关系怎么定、调用链怎么走。
- 本文按“先结构、后关系、再调用链”的顺序讲解。
2. 相关模块与文件位置(先看树)
value?.let(WeekType::valueOf)
**原因**:
- SQLite 无 ENUM 类型
- 枚举的 `name` 属性转换为字符串存储
- 反向转换使用 `valueOf()` 恢复枚举
**用途**:`CourseSessionEntity.weekType`(支持 ALL_WEEKS、ODD_WEEKS 等)
**重要注意**:若修改 WeekType 枚举值名称,数据库中的旧值会导致 `IllegalArgumentException`。数据库版本升级时须考虑迁移。
### 4. `List<Int>` ↔ JSON
```kotlin
private val json = Json
@TypeConverter
fun intListToJson(value: List<Int>?): String? =
value?.let { json.encodeToString(it) }
@TypeConverter
fun jsonToIntList(value: String?): List<Int>? =
value?.let { json.decodeFromString<List<Int>>(it) }原因:
- SQLite 无数组/LIST 类型
- 使用 Kotlin Serialization 将 List 编码为 JSON 字符串(如 "[1,3,5]")
- 便于灵活支持自定义周安排
使用场景:CourseSessionEntity.customWeeks(存储如 [1,3,5,7] 等自定义周数)
注册 TypeConverter:
在 SleepInDatabase 中:
@Database(
entities = [/* ... */],
version = 1,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class SleepInDatabase : RoomDatabase() {
// ...
}DAO 和 Flow 语义
什么是 DAO?
DAO(Data Access Object)是数据访问对象,定义所有与某张表交互的数据库操作。使用 DAO 的好处:
- 类型安全:编译时验证 SQL 正确性,而非运行时才发现错误
- 简洁 API:使用 Kotlin 注解而非手写 SQL
- 协程支持:原生
suspend函数和Flow异步返回
Flow 语义
Flow<T> 是 Kotlin 协程中的响应式数据流,当数据库数据变化时自动发出新值。
特点:
- 冷流(Cold Stream):只有 collect 时才执行查询
- 自动观察:DAO 自动订阅表的变化,数据更新时自动发出新值
- 生命周期感知:Compose 中使用
.collectAsStateWithLifecycle()自动感知 UI 生命周期
文件位置:app/src/main/java/com/kurosu/sleepin/data/local/dao/
TimetableDao 示例
文件位置:app/src/main/java/com/kurosu/sleepin/data/local/dao/TimetableDao.kt
观察所有课表(Flow)
@Query("SELECT * FROM timetables ORDER BY updatedAt DESC")
fun observeTimetables(): Flow<List<TimetableEntity>>使用场景:
- UI 需要展示所有课表列表
- 任何课表被添加/删除/修改时,Flow 自动发出新列表
Compose 中的使用:
val timetables by timetableDao.observeTimetables()
.collectAsStateWithLifecycle(initialValue = emptyList())
// timetables 变化时自动重组 UI观察当前活跃课表(Flow)
@Query("SELECT * FROM timetables WHERE isActive = 1 LIMIT 1")
fun observeActiveTimetable(): Flow<TimetableEntity?>为什么返回 TimetableEntity??
- 应用启动或未选择课表时,可能不存在活跃课表
- nullable 类型安全地表达"可能无数据"
一次性获取(suspend)
@Query("SELECT * FROM timetables WHERE id = :timetableId LIMIT 1")
suspend fun getById(timetableId: Long): TimetableEntity?与 Flow 的区别:
suspend fun是一次性操作,返回单个结果- 适合需要立即获取数据的场景(如点击"查看详情"按钮)
- 不适合持续观察数据变化
在 ViewModel 中的使用:
viewModelScope.launch {
val timetable = timetableDao.getById(id)
}