6、Room数据库:Room配置与Entity、DAO与Repository模式、Room与Flow结合
说实话,Room 是我在 Jetpack 里用得最爽的一个组件。为什么?因为它把 SQLite 那些繁琐的模板代码全给干掉了。你想想看,以前写个数据库操作,要写 SQLiteOpenHelper、ContentValues、Cursor……一堆东西。现在呢?一个注解搞定。
我个人习惯把 Room 看作是「带注解的 SQLite 封装」。它不是在底层替换 SQLite,而是让你用更优雅的方式去操作它。今天我们就来聊聊 Room 的核心玩法。
6.1 Room 配置:三步走
配置 Room 其实就三步:加依赖、写 Entity、写 DAO。咱们一步步来。
首先,在 build.gradle 里加上 Room 的依赖。注意要用 kapt 或者 ksp,因为 Room 需要编译时注解处理。
// 项目级 build.gradle
plugins {
id 'org.jetbrains.kotlin.kapt' version '1.9.0'
}
// 模块级 build.gradle
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
}
嗯,这里要注意版本号。我建议你每次用最新稳定版,因为 Room 团队一直在优化编译速度和运行时性能。
6.2 Entity:数据表的 Kotlin 化身
Entity 说白了就是一张表的 Kotlin 类。你定义一个数据类,加上 @Entity 注解,Room 就会自动帮你建表。
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "user_name")
val name: String,
val age: Int,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis()
)
我在项目中遇到过一个问题:有人把 @PrimaryKey 忘了加 autoGenerate = true,结果每次插入都要手动传 id,一传错就崩。所以我的建议是:只要 id 是自增的,就老老实实加上 autoGenerate。
6.3 DAO:数据访问对象
DAO 是 Room 的灵魂。你定义一个接口,写上 SQL 语句,Room 在编译时帮你生成实现类。
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): List<User>
@Insert
fun insertUser(user: User)
@Update
fun updateUser(user: User)
@Delete
fun deleteUser(user: User)
}
你看,就这么简单。@Insert、@Update、@Delete 这些注解,Room 会自动生成对应的 SQL。但 @Query 需要你自己写 SQL,这也是 Room 最灵活的地方。
6.4 Repository 模式:把 DAO 包一层
直接在你的 ViewModel 里调用 DAO 行不行?行,但不推荐。为什么?因为你的 ViewModel 不应该知道数据是从本地数据库来的,还是从网络来的。Repository 就是中间这一层。
class UserRepository(private val userDao: UserDao) {
fun getAllUsers(): List<User> {
return userDao.getAllUsers()
}
suspend fun insertUser(user: User) {
userDao.insertUser(user)
}
}
说白了,Repository 就是一个「数据源管理器」。它可能从 Room 读,也可能从 Retrofit 读。ViewModel 只管调用 repository.getXxx(),不用管数据从哪来。
我个人习惯在 Repository 里做「数据源切换」的逻辑。比如先读缓存,缓存没有再去网络请求,请求回来再存到 Room。这样 ViewModel 的代码就非常干净。
6.5 Room 与 Flow 结合:响应式数据库
这是 Room 最酷的地方。你想想看,以前要监听数据库变化,得写 ContentObserver 或者自己搞回调。现在呢?直接返回 Flow 就行。
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun observeAllUsers(): Flow<List<User>>
@Insert
suspend fun insertUser(user: User)
}
注意看,这里返回的是 Flow<List<User>>。每次数据库里的 users 表有变化,Room 会自动发射一个新的 List。你的 UI 层订阅这个 Flow,就能实时更新。
为什么会这样?因为 Room 在底层用了 SQLite 的触发器机制。每次表数据变化,Room 会重新执行查询,然后把结果通过 Flow 发出来。你想想看,这比你自己写轮询优雅了多少倍?
// 在 ViewModel 里
class UserViewModel(private val repository: UserRepository) : ViewModel() {
val users: StateFlow<List<User>> = repository.observeAllUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
这里我用了 stateIn 把 Flow 转成 StateFlow。为什么要转?因为 UI 层需要「当前值」,而 Flow 是冷流,不订阅就不执行。StateFlow 会保留最后一个值,UI 重建时直接拿,不用等数据库重新查询。
6.6 避坑指南
最后分享几个我踩过的坑:
- 不要在 DAO 里用 LiveData 返回复杂查询:LiveData 的回调在 UI 线程,复杂查询会卡 UI。用 Flow 可以指定调度器。
- @Transaction 别忘了加:如果你在一个方法里操作多个表,记得加 @Transaction 注解。我曾经没加,结果并发插入时数据不一致……
- 数据库迁移要小心:Room 的 migration 机制很强大,但写错了会丢数据。我建议每次改表结构前,先写个测试用例验证迁移逻辑。
好了,Room 的核心内容就这些。配置、Entity、DAO、Repository、Flow,这几个概念串起来,就是一个完整的本地数据层。你想想看,以前写 SQLite 要多少代码?现在用 Room,一个注解搞定建表,一个接口搞定 CRUD,一个 Flow 搞定实时更新。这就是 Jetpack 的威力。