Room 是什么
目录
- Room 是什么
- 本地存储选型对比
- 核心架构:三个必学的概念
- 环境搭建
- Entity — 定义数据表
- DAO — 定义数据操作
- Database — 组装数据库
- 基础 CRUD 操作
- Room + Flow:实时数据流
- Room + ViewModel:完整闭环
- 表关系:一对多 & 多对多
- 数据库迁移
- Flutter 开发者速查对照
1. Room 是什么
Room 是 Google Jetpack 提供的 SQLite ORM 框架,它在原始 SQLite API 上加了一层抽象,让你用 Kotlin 注解代替手写 SQL 模板代码。
用一句话类比:Room = Android 版的 sqflite + drift(原 moor)的结合体——既有 Flutter drift 的类型安全和代码生成,又有 sqflite 的 SQLite 底层能力。
Room 解决了什么问题?
| 原始 SQLite 的痛点 | Room 的解法 |
|---|---|
大量模板代码(ContentValues、Cursor) | 注解驱动,编译期自动生成代码 |
| 运行时 SQL 错误 | 编译期检查 SQL 语法 |
| 主线程操作数据库导致 ANR | 强制要求异步,配合协程 |
| 数据变化无法实时感知 | 原生支持返回 Flow,数据变化自动推送 |
| 跨版本升级复杂 | 内置 Migration 迁移机制 |
2. 本地存储选型对比
| 方案 | 适用场景 | Flutter 类比 |
|---|---|---|
| Room | 结构化数据、复杂查询、关联表 | drift / sqflite |
| DataStore Preferences | 键值对设置项(用户偏好) | shared_preferences |
| DataStore Proto | 类型安全的键值对 | hive(基础类型) |
| 文件存储 | 大文件、图片、音频 | path_provider + dart:io |
| EncryptedSharedPreferences | 加密键值对(token、密钥) | flutter_secure_storage |
选型原则:需要查询、排序、关联 → Room;简单设置项 → DataStore;敏感信息 → EncryptedSharedPreferences。
3. 核心架构:三个必学的概念
┌─────────────────────────────────────────────┐
│ Your Application │
│ │
│ ViewModel / Repository │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ DAO │ ← 你只需要写这个接口 │
│ │ (接口定义) │ │
│ └──────┬──────┘ │
│ │ Room 编译期自动生成实现 │
│ ▼ │
│ ┌─────────────┐ │
│ │ Database │ ← 数据库单例 │
│ │ (抽象类) │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Entity │ ← 一个 Entity = 一张表 │
│ │ (数据类) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
| 概念 | 职责 | Flutter drift 类比 |
|---|---|---|
@Entity | 定义数据表结构(字段、主键、索引) | class Tasks extends Table |
@Dao | 定义增删改查接口(SQL 在这里写) | abstract class TasksDao |
@Database | 组装数据库(关联 Entity 和 DAO) | AppDatabase extends _$AppDatabase |
4. 环境搭建
build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "1.9.22-1.0.17" // KSP 代码生成
}
dependencies {
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion") // 协程 + Flow 支持
ksp("androidx.room:room-compiler:$roomVersion") // 编译期代码生成
// 可选:Room 测试支持
testImplementation("androidx.room:room-testing:$roomVersion")
}
注意:必须用
ksp而不是kapt,KSP 编译速度快 2 倍以上。
5. Entity — 定义数据表
@Entity 注解的 Kotlin 数据类 = 一张数据库表,每个字段 = 一列。
基础 Entity
@Entity(tableName = "users") // 指定表名,不写默认用类名小写
data class UserEntity(
@PrimaryKey(autoGenerate = true) // 自增主键,类比 SQL: INTEGER PRIMARY KEY AUTOINCREMENT
val id: Int = 0,
@ColumnInfo(name = "display_name") // 自定义列名(不写则用字段名)
val name: String,
val email: String,
val avatarUrl: String? = null, // 可空字段 → 列允许 NULL
val createdAt: Long = System.currentTimeMillis()
)
带索引和唯一约束
@Entity(
tableName = "articles",
indices = [
Index(value = ["slug"], unique = true), // slug 唯一索引
Index(value = ["author_id"]), // author_id 普通索引(加速查询)
Index(value = ["category", "published_at"]) // 联合索引
]
)
data class ArticleEntity(
@PrimaryKey val id: String, // 字符串主键(如 UUID)
val title: String,
val slug: String, // 唯一,不能重复
@ColumnInfo(name = "author_id")
val authorId: Int,
val category: String,
@ColumnInfo(name = "published_at")
val publishedAt: Long,
val content: String,
val isBookmarked: Boolean = false
)
复合主键
@Entity(
tableName = "user_roles",
primaryKeys = ["user_id", "role_id"] // 联合主键
)
data class UserRoleEntity(
@ColumnInfo(name = "user_id") val userId: Int,
@ColumnInfo(name = "role_id") val roleId: Int,
val assignedAt: Long = System.currentTimeMillis()
)
类型转换器(存储复杂类型)
Room 原生只支持基础类型。存 List、Date、自定义对象需要 TypeConverter:
// 定义转换器
class Converters {
// List<String> ↔ JSON 字符串
@TypeConverter
fun fromStringList(value: List<String>): String =
Gson().toJson(value)
@TypeConverter
fun toStringList(value: String): List<String> =
Gson().fromJson(value, object : TypeToken<List<String>>() {}.type)
// Date ↔ Long 时间戳
@TypeConverter
fun fromDate(date: Date?): Long? = date?.time
@TypeConverter
fun toDate(timestamp: Long?): Date? =
timestamp?.let { Date(it) }
}
// 在 Database 类上声明使用
@TypeConverters(Converters::class)
@Database(...)
abstract class AppDatabase : RoomDatabase() { ... }
6. DAO — 定义数据操作
DAO(Data Access Object)是一个 interface 或 abstract class,用注解描述 SQL,Room 编译期自动生成实现。
基础 CRUD
@Dao
interface UserDao {
// ── INSERT ─────────────────────────────────────────
@Insert(onConflict = OnConflictStrategy.REPLACE) // 冲突时替换(类比 upsert)
suspend fun insert(user: UserEntity): Long // 返回新行的 rowId
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAll(users: List<UserEntity>): List<Long>
// ── UPDATE ─────────────────────────────────────────
@Update
suspend fun update(user: UserEntity): Int // 返回影响行数
// ── DELETE ─────────────────────────────────────────
@Delete
suspend fun delete(user: UserEntity): Int
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteById(userId: Int): Int
@Query("DELETE FROM users")
suspend fun deleteAll()
// ── QUERY ──────────────────────────────────────────
@Query("SELECT * FROM users ORDER BY display_name ASC")
fun getAllUsers(): Flow<List<UserEntity>> // Flow:数据变化自动推送
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: Int): UserEntity? // suspend:一次性查询
@Query("SELECT * FROM users WHERE email = :email LIMIT 1")
suspend fun getUserByEmail(email: String): UserEntity?
// 模糊搜索
@Query("SELECT * FROM users WHERE display_name LIKE '%' || :query || '%'")
fun searchUsers(query: String): Flow<List<UserEntity>>
// 分页查询
@Query("SELECT * FROM users ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
suspend fun getUsersPaged(limit: Int, offset: Int): List<UserEntity>
}
高级查询技巧
@Dao
interface ArticleDao {
// 返回部分字段(投影查询),用数据类接收
@Query("SELECT id, title, published_at FROM articles WHERE category = :cat")
fun getArticleSummaries(cat: String): Flow<List<ArticleSummary>>
// 多条件动态查询
@Query("""
SELECT * FROM articles
WHERE (:category IS NULL OR category = :category)
AND (:authorId IS NULL OR author_id = :authorId)
AND published_at >= :fromTime
ORDER BY published_at DESC
""")
fun filterArticles(
category: String?,
authorId: Int?,
fromTime: Long
): Flow<List<ArticleEntity>>
// Upsert(Room 2.5.0+)
@Upsert
suspend fun upsert(article: ArticleEntity)
// 批量 Upsert
@Upsert
suspend fun upsertAll(articles: List<ArticleEntity>)
// 事务(多个操作原子执行)
@Transaction
suspend fun replaceAll(articles: List<ArticleEntity>) {
deleteAll()
insertAll(articles)
}
@Insert
suspend fun insertAll(articles: List<ArticleEntity>)
@Query("DELETE FROM articles")
suspend fun deleteAll()
}
// 投影查询的接收数据类(不需要 @Entity)
data class ArticleSummary(
val id: String,
val title: String,
@ColumnInfo(name = "published_at") val publishedAt: Long
)
7. Database — 组装数据库
@Database(
entities = [
UserEntity::class,
ArticleEntity::class,
UserRoleEntity::class,
],
version = 1, // 数据库版本,升级时递增
exportSchema = true // 导出 schema.json 用于迁移验证(推荐 true)
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
// 每个 DAO 定义一个抽象方法
abstract fun userDao(): UserDao
abstract fun articleDao(): ArticleDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
// 双重检查锁定单例模式
return INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database.db" // 数据库文件名
)
.fallbackToDestructiveMigration() // 开发期用:版本不匹配时清库重建
// .addMigrations(MIGRATION_1_2) // 生产环境用迁移脚本
.build()
.also { INSTANCE = it }
}
}
}
}
生产建议:用 Hilt 依赖注入管理 Database 单例,而不是手写 companion object。
Hilt 注入版本
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.build()
@Provides
fun provideUserDao(db: AppDatabase): UserDao = db.userDao()
@Provides
fun provideArticleDao(db: AppDatabase): ArticleDao = db.articleDao()
}
8. 基础 CRUD 操作
Repository 层封装
class UserRepository @Inject constructor(
private val userDao: UserDao
) {
// 查询(Flow,实时)
val allUsers: Flow<List<UserEntity>> = userDao.getAllUsers()
// 查询(单次)
suspend fun getUserById(id: Int): UserEntity? = userDao.getUserById(id)
// 新增
suspend fun addUser(name: String, email: String): Long {
val entity = UserEntity(name = name, email = email)
return userDao.insert(entity)
}
// 修改
suspend fun updateUser(user: UserEntity) = userDao.update(user)
// 删除
suspend fun deleteUser(user: UserEntity) = userDao.delete(user)
// 搜索
fun searchUsers(query: String): Flow<List<UserEntity>> =
userDao.searchUsers(query)
}
在 ViewModel 中调用
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
val users: StateFlow<List<UserEntity>> = repository.allUsers
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun addUser(name: String, email: String) {
viewModelScope.launch {
repository.addUser(name, email)
// users Flow 会自动更新,无需手动刷新!
}
}
fun deleteUser(user: UserEntity) {
viewModelScope.launch {
repository.deleteUser(user)
}
}
}
在 Compose UI 中展示
@Composable
fun UserListScreen(viewModel: UserViewModel = hiltViewModel()) {
val users by viewModel.users.collectAsStateWithLifecycle()
LazyColumn {
items(users, key = { it.id }) { user ->
UserItem(
user = user,
onDelete = { viewModel.deleteUser(user) }
)
}
}
}
9. Room + Flow:实时数据流
这是 Room 最强大的特性:查询返回 Flow,数据库内容变化时 Flow 自动发射新数据。
数据库写入操作
│
▼
Room 检测到表数据变化
│
▼
Flow 自动 emit 新数据
│
▼
ViewModel 的 StateFlow 更新
│
▼
Compose UI 自动重组
@Dao
interface NoteDao {
// 返回 Flow:Room 会在 notes 表数据变化时自动重新查询并 emit
@Query("SELECT * FROM notes ORDER BY updated_at DESC")
fun getAllNotes(): Flow<List<NoteEntity>>
// 配合 combine 合并多个查询结果
@Query("SELECT COUNT(*) FROM notes WHERE is_pinned = 1")
fun getPinnedCount(): Flow<Int>
}
// ViewModel 中合并两个 Flow
val uiState: StateFlow<NoteUiState> = combine(
noteDao.getAllNotes(),
noteDao.getPinnedCount()
) { notes, pinnedCount ->
NoteUiState(notes = notes, pinnedCount = pinnedCount)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), NoteUiState())
10. Room + ViewModel:完整闭环
以 Todo 应用为例,展示从数据库到 UI 的完整链路:
Entity
@Entity(tableName = "todos")
data class TodoEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val description: String = "",
val isCompleted: Boolean = false,
val priority: Int = 0, // 0=低 1=中 2=高
val createdAt: Long = System.currentTimeMillis()
)
DAO
@Dao
interface TodoDao {
@Query("SELECT * FROM todos ORDER BY priority DESC, created_at DESC")
fun getAllTodos(): Flow<List<TodoEntity>>
@Query("SELECT * FROM todos WHERE is_completed = :completed")
fun getTodosByStatus(completed: Boolean): Flow<List<TodoEntity>>
@Upsert
suspend fun upsert(todo: TodoEntity)
@Delete
suspend fun delete(todo: TodoEntity)
@Query("UPDATE todos SET is_completed = :completed WHERE id = :id")
suspend fun updateStatus(id: Int, completed: Boolean)
@Query("SELECT COUNT(*) FROM todos WHERE is_completed = 0")
fun getPendingCount(): Flow<Int>
}
Repository
class TodoRepository @Inject constructor(private val dao: TodoDao) {
val allTodos: Flow<List<TodoEntity>> = dao.getAllTodos()
val pendingCount: Flow<Int> = dao.getPendingCount()
suspend fun addTodo(title: String, priority: Int) =
dao.upsert(TodoEntity(title = title, priority = priority))
suspend fun toggleTodo(todo: TodoEntity) =
dao.updateStatus(todo.id, !todo.isCompleted)
suspend fun deleteTodo(todo: TodoEntity) = dao.delete(todo)
}
ViewModel
@HiltViewModel
class TodoViewModel @Inject constructor(
private val repository: TodoRepository
) : ViewModel() {
data class UiState(
val todos: List<TodoEntity> = emptyList(),
val pendingCount: Int = 0
)
val uiState: StateFlow<UiState> = combine(
repository.allTodos,
repository.pendingCount
) { todos, count ->
UiState(todos = todos, pendingCount = count)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState())
fun addTodo(title: String, priority: Int = 1) {
if (title.isBlank()) return
viewModelScope.launch { repository.addTodo(title, priority) }
}
fun toggleTodo(todo: TodoEntity) {
viewModelScope.launch { repository.toggleTodo(todo) }
}
fun deleteTodo(todo: TodoEntity) {
viewModelScope.launch { repository.deleteTodo(todo) }
}
}
Compose UI
@Composable
fun TodoScreen(viewModel: TodoViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
var inputText by remember { mutableStateOf("") }
Scaffold(
topBar = {
TopAppBar(title = { Text("待办 (${state.pendingCount} 未完成)") })
}
) { padding ->
Column(modifier = Modifier.padding(padding)) {
// 输入框
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
placeholder = { Text("新建待办...") },
modifier = Modifier.weight(1f),
singleLine = true
)
Button(onClick = {
viewModel.addTodo(inputText)
inputText = ""
}) { Text("添加") }
}
// 列表
LazyColumn {
items(state.todos, key = { it.id }) { todo ->
TodoItem(
todo = todo,
onToggle = { viewModel.toggleTodo(todo) },
onDelete = { viewModel.deleteTodo(todo) }
)
}
}
}
}
}
@Composable
fun TodoItem(
todo: TodoEntity,
onToggle: () -> Unit,
onDelete: () -> Unit
) {
ListItem(
headlineContent = {
Text(
text = todo.title,
textDecoration = if (todo.isCompleted)
TextDecoration.LineThrough else TextDecoration.None
)
},
leadingContent = {
Checkbox(checked = todo.isCompleted, onCheckedChange = { onToggle() })
},
trailingContent = {
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "删除")
}
}
)
}
11. 表关系:一对多 & 多对多
一对多(One-to-Many)
// 一个 User 有多个 Post
@Entity(tableName = "posts")
data class PostEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "user_id") val userId: Int, // 外键
val title: String,
val content: String
)
// 关联查询结果
data class UserWithPosts(
@Embedded val user: UserEntity, // @Embedded:展开嵌套对象的字段
@Relation(
parentColumn = "id", // UserEntity 的主键
entityColumn = "user_id" // PostEntity 的外键
)
val posts: List<PostEntity>
)
// DAO 查询
@Dao
interface UserWithPostsDao {
@Transaction // 关联查询必须加 @Transaction
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserWithPosts(userId: Int): Flow<UserWithPosts?>
@Transaction
@Query("SELECT * FROM users")
fun getAllUsersWithPosts(): Flow<List<UserWithPosts>>
}
多对多(Many-to-Many)
// 学生 & 课程:一个学生选多门课,一门课有多个学生
@Entity(tableName = "students")
data class StudentEntity(@PrimaryKey val id: Int, val name: String)
@Entity(tableName = "courses")
data class CourseEntity(@PrimaryKey val id: Int, val title: String)
// 中间关联表
@Entity(
tableName = "student_course",
primaryKeys = ["student_id", "course_id"]
)
data class StudentCourseCrossRef(
@ColumnInfo(name = "student_id") val studentId: Int,
@ColumnInfo(name = "course_id") val courseId: Int
)
// 关联结果(学生 + 所选课程)
data class StudentWithCourses(
@Embedded val student: StudentEntity,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = StudentCourseCrossRef::class,
parentColumn = "student_id",
entityColumn = "course_id"
)
)
val courses: List<CourseEntity>
)
@Dao
interface StudentDao {
@Transaction
@Query("SELECT * FROM students WHERE id = :studentId")
fun getStudentWithCourses(studentId: Int): Flow<StudentWithCourses?>
}
12. 数据库迁移
每次修改 Entity(加字段、改类型)都必须升级版本并提供迁移脚本:
// 版本 1 → 版本 2:users 表增加 phone 字段
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE users ADD COLUMN phone TEXT"
)
}
}
// 版本 2 → 版本 3:新增 tags 表
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#000000'
)
""")
}
}
// 注册迁移脚本
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
开发期快捷方式:
.fallbackToDestructiveMigration()会在版本不匹配时自动删库重建,仅用于开发阶段,生产环境必须写 Migration。
13. Flutter 开发者速查对照
| 概念 | Flutter (drift) | Android Room |
|---|---|---|
| 表定义 | class Tasks extends Table | @Entity data class TaskEntity |
| 主键 | IntColumn get id => integer().autoIncrement()() | @PrimaryKey(autoGenerate = true) val id: Int |
| DAO | abstract class TasksDao | @Dao interface TaskDao |
| 插入 | into(tasks).insert(task) | @Insert suspend fun insert(task: TaskEntity) |
| 查询全部 | select(tasks).watch() | @Query("SELECT * FROM tasks") fun getAll(): Flow<List<TaskEntity>> |
| 条件查询 | (select(tasks)..where((t) => t.done.equals(false))).watch() | @Query("SELECT * FROM tasks WHERE is_done = 0") fun getPending(): Flow<...> |
| 实时更新 | Stream<List<Task>> | Flow<List<TaskEntity>> |
| 数据库类 | @DriftDatabase(tables: [Tasks]) | @Database(entities = [TaskEntity::class]) |
| 迁移 | MigrationStrategy | Migration(from, to) |
| 类型转换 | TypeConverter | @TypeConverter |
| 事务 | transaction(() async { ... }) | @Transaction suspend fun ... |
快速参考:常用注解速查
// Entity 相关
@Entity(tableName = "table_name") // 声明数据表
@PrimaryKey(autoGenerate = true) // 自增主键
@ColumnInfo(name = "column_name") // 自定义列名
@Ignore // 忽略此字段(不映射到数据库)
@Embedded // 嵌套对象展开到同一张表
@Relation(parentColumn, entityColumn) // 声明表关系
// DAO 相关
@Dao // 声明 DAO 接口
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Update
@Delete
@Upsert // Room 2.5.0+
@Query("SELECT ...") // 自定义 SQL
@Transaction // 原子事务
// Database 相关
@Database(entities = [...], version = N)
@TypeConverters(Converters::class)
推荐学习路径:
UserEntity+UserDao+AppDatabase三件套跑通 → 加入 Flow 实时监听 → 接入 ViewModel → 理解表关系 → 学习迁移机制。整个过程和 Flutter drift 的心智模型几乎一致,上手非常快。