Room 入门——实体 / DAO / 数据库搭建与增删改查
各位 Android 开发者好,欢迎来到《Jetpack 内核实战》系列第 5 篇。 在前四篇中,我们完成了 ViewModel、LiveData、协程和 Repository 分层架构的搭建,项目已经具备了清晰的骨架。但美中不足的是——所有数据都存储在内存中,App 一退出,数据就全没了。 本篇我们将正式引入 Room 数据库,这是 Google 官方推荐的 SQLite 抽象层,它将繁琐的数据库操作简化为注解驱动的优雅 API,并提供编译时 SQL 校验,让数据库开发变得安全高效。我们将从零搭建记事本数据库,完成增删改查全功能实战,所有代码均遵循 Kotlin 规范与 Version Catalog 依赖管理,可直接编译运行。
一、Room 是什么?为什么放弃原生 SQLite?
原生 SQLite 是 Android 自带的数据库能力,但开发体验极差。Room 在保留 SQLite 全部能力的基础上,做了大量工程化优化,完美适配 Jetpack 架构。
1. 原生 SQLite 的核心痛点
| 痛点 | 说明 |
|---|---|
| 样板代码极多 | 需要手动管理数据库帮助类、游标解析、SQL 拼接,一个简单的查询就要写几十行代码 |
| 无编译时校验 | SQL 语法错误、表名字段名写错,只有运行时才会崩溃,排查成本极高 |
| 线程安全无保障 | 需要自己处理主线程访问、多线程并发、事务管理,稍有不慎就会出现 ANR 或数据错乱 |
| 无响应式支持 | 数据变化后需要手动刷新 UI,无法和 LiveData/Flow 无缝配合 |
2. Room 的核心优势
Room 是基于注解的 ORM(对象关系映射)框架,核心优势完美解决原生 SQLite 的痛点:
| 优势 | 说明 |
|---|---|
| 注解式开发,极简代码 | 通过注解定义表、接口定义操作,无需手写 SQL 帮助类与游标解析 |
| 编译时 SQL 校验 | 所有 SQL 语句在编译阶段就会检查语法、表名、字段名,提前发现问题,杜绝运行时崩溃 |
| 协程/LiveData 原生支持 | DAO 方法直接支持 suspend 挂起函数、返回 LiveData/Flow,和 Jetpack 架构无缝融合 |
| 线程安全封装 | 自动处理数据库操作的线程切换,避免主线程访问异常,事务管理更简单 |
3. Room 三大核心组件
Room 的架构由三个核心部分组成,各司其职,共同完成对象到数据库表的映射:
| 组件 | 注解 | 核心职责 |
|---|---|---|
| 实体类 | @Entity | 对应数据库中的一张表,类的属性对应表的字段 |
| 数据访问对象 DAO | @Dao | 定义数据库的操作接口(增删改查),是业务层操作数据库的唯一入口 |
| 数据库类 | @Database | 数据库的持有者,管理数据库版本与所有 DAO 实例 |
三者的依赖关系:Database 包含 DAO,DAO 操作 Entity,业务层通过 Database 获取 DAO,再通过 DAO 操作实体数据,完全符合分层架构的设计思想。
二、前置依赖配置
1. Version Catalog 配置
检查 gradle/libs.versions.toml 是否包含以下配置:
[versions]
room = "2.6.1"
[libraries]
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
[plugins]
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
2. 模块级 build.gradle.kts 配置
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.kapt) // 启用 kapt,用于注解处理
}
android {
// ... 其他配置
kapt {
correctErrorTypes = true // 修正错误类型,提升 Room 稳定性
}
}
dependencies {
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx) // 协程 + Flow 支持
kapt(libs.androidx.room.compiler) // 注解处理器
}
注意:kapt 是 Kotlin 的注解处理工具,Room 通过它在编译期生成代码。如果不添加,@Dao 和 @Database 的代码不会生成,运行时会出现找不到实现类的错误。
三、@Entity:实体定义与常用配置
实体类是数据库表的映射,一个数据类对应一张表,通过注解可以灵活配置表名、主键、字段、忽略项等属性。
1. 基础实体定义
最基础的实体类只需要添加 @Entity 注解,并用 @PrimaryKey 定义主键:
package com.example.jetpacknote.data.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes") // tableName 自定义表名,不写则默认用类名
data class Note(
@PrimaryKey(autoGenerate = true) // 主键自增
val id: Long = 0,
val content: String,
val createTime: Long = System.currentTimeMillis(),
val updateTime: Long = System.currentTimeMillis()
)
2. 常用字段配置
(1)自定义字段名:@ColumnInfo
如果需要表字段名和类属性名不一致,可以用 @ColumnInfo 映射,同时可以配置字段默认值:
@ColumnInfo(name = "note_content", defaultValue = "")
val content: String
@ColumnInfo(name = "create_time")
val createTime: Long
(2)忽略字段:@Ignore
如果某个属性只用于业务逻辑,不需要存入数据库,添加 @Ignore 注解即可:
@Ignore
val isSelected: Boolean = false
(3)嵌套对象:@Embedded
如果实体中有多个字段可以归类为一个对象,可以用 @Embedded 嵌套实体,表中依然是独立的字段,但代码结构更清晰。
data class TimeInfo(
val createTime: Long,
val updateTime: Long
)
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val content: String,
@Embedded
val timeInfo: TimeInfo // 表中会生成 createTime、updateTime 两个字段
)
注意:嵌套对象的字段名不能和外层实体重复。可以通过 prefix 属性给嵌套字段加前缀解决:
@Embedded(prefix = "time_") // 表字段变成 time_createTime、time_updateTime
val timeInfo: TimeInfo
3. 实体类定义规范总结
| 规范 | 说明 | 示例 |
|---|---|---|
| 使用 data class | 自动生成 equals/hashCode/toString | data class Note(...) |
| 主键必须有默认值 | 配合 autoGenerate = true 时 | val id: Long = 0 |
| 字段用 val 保持不可变 | 数据类推荐不可变 | val content: String |
| 非存储字段加 @Ignore | 避免 Room 尝试映射 | @Ignore val temp: String? |
| 用 @ColumnInfo 显式命名 | 避免重构时字段名混乱 | @ColumnInfo(name = "create_time") |
四、@Dao:四大核心操作
DAO 是 Data Access Object 的缩写,是数据库操作的接口层,业务层只和 DAO 交互,不需要关心底层 SQL 实现。Room 会在编译时自动生成 DAO 接口的实现类。
1. DAO 基础定义
@Dao
interface NoteDao {
// 数据库操作方法都定义在这里
}
2. @Insert:插入数据
直接标记插入方法,Room 自动生成插入 SQL,支持单个对象、列表对象批量插入:
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: Note): Long // 返回插入后的自增 ID
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNotes(notes: List<Note>) // 批量插入
冲突策略(OnConflictStrategy):
| 策略 | 行为 |
|---|---|
| REPLACE | 主键重复时替换旧数据 |
| IGNORE | 主键重复时忽略新数据 |
| ABORT | 主键重复时终止操作,抛出异常(默认值) |
3. @Update:更新数据
根据主键匹配更新对应的数据行:
@Update
suspend fun updateNote(note: Note): Int // 返回受影响的行数
4. @Delete:删除数据
根据主键匹配删除对应的数据行:
@Delete
suspend fun deleteNote(note: Note): Int // 返回受影响的行数
5. @Query:自定义查询
@Query 是最灵活的操作注解,可以自定义 SQL 语句,支持所有 SQLite 语法,编译时会校验 SQL 正确性:
@Dao
interface NoteDao {
// 查询所有笔记(按时间倒序)
@Query("SELECT * FROM notes ORDER BY createTime DESC")
suspend fun getAllNotes(): List<Note>
// 根据 ID 查询单条笔记
@Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getNoteById(noteId: Long): Note?
// 模糊搜索笔记
@Query("SELECT * FROM notes WHERE content LIKE '%' || :keyword || '%'")
suspend fun searchNotes(keyword: String): List<Note>
// 删除所有笔记
@Query("DELETE FROM notes")
suspend fun clearAllNotes()
}
重要规范:DAO 中的方法推荐全部定义为 suspend 挂起函数,Room 会自动在后台线程执行数据库操作,无需手动切线程,完美适配 Kotlin 协程。
五、@TypeConverter:处理复杂类型
SQLite 只支持整数、字符串、浮点等基础数据类型,如果实体中有 Date、List、自定义对象等复杂类型,就需要用 @TypeConverter 做类型转换。
1. Date ↔ Long 转换器
package com.example.jetpacknote.data.converter
import androidx.room.TypeConverter
import java.util.Date
class DateConverter {
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
@TypeConverter
fun timestampToDate(timestamp: Long?): Date? = timestamp?.let { Date(it) }
}
2. List ↔ JSON 转换器(使用 Gson)
如果需要存储 List 等集合类型,可以用 Gson 序列化为 JSON 字符串:
package com.example.jetpacknote.data.converter
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class StringListConverter {
private val gson = Gson()
@TypeConverter
fun listToString(list: List<String>?): String? {
return list?.let { gson.toJson(it) }
}
@TypeConverter
fun stringToList(str: String?): List<String>? {
return str?.let {
val type = object : TypeToken<List<String>>() {}.type
gson.fromJson(it, type)
}
}
}
3. 注册类型转换器
写好转换器后,需要在 Database 数据库类上通过 @TypeConverters 注解注册:
@Database(entities = [Note::class], version = 1)
@TypeConverters(DateConverter::class, StringListConverter::class)
abstract class AppDatabase : RoomDatabase() {
// ...
}
六、@Database:数据库类定义
@Database 注解用于定义数据库类,它是 Room 的入口点,采用单例模式确保全局只有一个数据库实例。
package com.example.jetpacknote.data.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.example.jetpacknote.data.converter.DateConverter
import com.example.jetpacknote.data.dao.NoteDao
import com.example.jetpacknote.data.entity.Note
@Database(
entities = [Note::class],
version = 1,
exportSchema = false
)
@TypeConverters(DateConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
private const val DATABASE_NAME = "jetpack_note_db"
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext, // 必须用 Application Context
AppDatabase::class.java,
DATABASE_NAME
).build().also { INSTANCE = it }
}
}
}
}
关键要点:
- 必须使用 Application Context,不能传入 Activity Context,否则会造成内存泄漏
- 数据库实例必须是单例,避免重复创建连接
- @Volatile + synchronized 保证多线程安全
七、实战:记事本完整 CRUD
现在将所有概念整合到记事本项目中,完整走一遍从建库到增删改查的全流程。
步骤 1:创建 Note 实体类
package com.example.jetpacknote.data.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.Date
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val content: String,
val createTime: Date = Date(),
val updateTime: Date = Date()
)
步骤 2:创建 NoteDao
package com.example.jetpacknote.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.jetpacknote.data.entity.Note
@Dao
interface NoteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: Note): Long
@Update
suspend fun updateNote(note: Note): Int
@Delete
suspend fun deleteNote(note: Note): Int
@Query("SELECT * FROM notes ORDER BY createTime DESC")
suspend fun getAllNotes(): List<Note>
@Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getNoteById(noteId: Long): Note?
@Query("DELETE FROM notes")
suspend fun clearAllNotes()
}
步骤 3:创建 AppDatabase(带转换器)
package com.example.jetpacknote.data.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.example.jetpacknote.data.converter.DateConverter
import com.example.jetpacknote.data.dao.NoteDao
import com.example.jetpacknote.data.entity.Note
@Database(
entities = [Note::class],
version = 1,
exportSchema = false
)
@TypeConverters(DateConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"jetpack_note_db"
).build().also { INSTANCE = it }
}
}
}
}
步骤 4:创建 DateConverter
package com.example.jetpacknote.data.converter
import androidx.room.TypeConverter
import java.util.Date
class DateConverter {
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
@TypeConverter
fun timestampToDate(timestamp: Long?): Date? = timestamp?.let { Date(it) }
}
步骤 5:改造 Repository(从内存到数据库)
这是分层架构的优势体现:Repository 内部从模拟数据切换为 Room 数据库,对外方法签名完全不变,ViewModel 和 UI 层不需要做任何修改。
package com.example.jetpacknote.data.repository
import android.content.Context
import com.example.jetpacknote.data.database.AppDatabase
import com.example.jetpacknote.data.entity.Note
import java.util.Date
class NoteRepository private constructor(context: Context) {
private val noteDao = AppDatabase.getInstance(context).noteDao()
suspend fun getAllNotes(): Result<List<Note>> {
return try {
Result.success(noteDao.getAllNotes())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getNoteById(id: Long): Result<Note?> {
return try {
Result.success(noteDao.getNoteById(id))
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun addNote(content: String): Result<Boolean> {
return try {
val note = Note(content = content)
noteDao.insertNote(note)
Result.success(true)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun updateNote(note: Note): Result<Boolean> {
return try {
val updated = note.copy(updateTime = Date())
noteDao.updateNote(updated)
Result.success(true)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun deleteNote(note: Note): Result<Boolean> {
return try {
noteDao.deleteNote(note)
Result.success(true)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun clearAllNotes(): Result<Unit> {
return try {
noteDao.clearAllNotes()
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
companion object {
@Volatile
private var INSTANCE: NoteRepository? = null
fun getInstance(context: Context): NoteRepository {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: NoteRepository(context.applicationContext).also { INSTANCE = it }
}
}
}
}
步骤 6:更新 ViewModel
package com.example.jetpacknote.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.jetpacknote.data.repository.NoteRepository
import kotlinx.coroutines.launch
sealed class NoteUiState {
data object Loading : NoteUiState()
data class Success(val notes: List<String>) : NoteUiState()
data class Error(val message: String) : NoteUiState()
}
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repository = NoteRepository.getInstance(application)
private val _uiState = MutableLiveData<NoteUiState>()
val uiState: LiveData<NoteUiState> = _uiState
fun loadNotes() {
viewModelScope.launch {
_uiState.value = NoteUiState.Loading
val result = repository.getAllNotes()
result.onSuccess { notes ->
_uiState.value = NoteUiState.Success(notes.map { it.content })
}.onFailure { error ->
_uiState.value = NoteUiState.Error(error.message ?: "加载失败")
}
}
}
fun addNote(content: String) {
viewModelScope.launch {
repository.addNote(content)
loadNotes() // 添加后自动刷新列表
}
}
fun deleteNote(content: String) {
viewModelScope.launch {
// 简单演示:删除所有匹配内容的第一条
val result = repository.getAllNotes()
result.onSuccess { notes ->
notes.firstOrNull { it.content == content }?.let {
repository.deleteNote(it)
loadNotes()
}
}
}
}
}
为什么继承 AndroidViewModel? 因为 Repository 需要 Context,而 AndroidViewModel 持有 Application Context,安全且不会内存泄漏。
步骤 7:更新 Activity 布局
在 activity_main.xml 中添加输入框和添加按钮:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/etInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="输入笔记内容"
android:inputType="textMultiLine" />
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加" />
</LinearLayout>
<Button
android:id="@+id/btnRefresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="刷新列表" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone" />
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:textSize="16sp"
android:visibility="gone" />
<TextView
android:id="@+id/tvError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:textColor="#D32F2F"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
步骤 8:更新 Activity
package com.example.jetpacknote
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.jetpacknote.databinding.ActivityMainBinding
import com.example.jetpacknote.ui.NoteUiState
import com.example.jetpacknote.ui.NoteViewModel
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val viewModel: NoteViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initClickListeners()
observeUiState()
viewModel.loadNotes()
}
private fun initClickListeners() {
binding.btnAdd.setOnClickListener {
val content = binding.etInput.text.toString().trim()
if (content.isEmpty()) {
Toast.makeText(this, "请输入笔记内容", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
viewModel.addNote(content)
binding.etInput.text?.clear()
}
binding.btnRefresh.setOnClickListener {
viewModel.loadNotes()
}
}
private fun observeUiState() {
viewModel.uiState.observe(this) { state ->
when (state) {
is NoteUiState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
binding.tvContent.visibility = View.GONE
binding.tvError.visibility = View.GONE
}
is NoteUiState.Success -> {
binding.progressBar.visibility = View.GONE
binding.tvError.visibility = View.GONE
binding.tvContent.visibility = View.VISIBLE
binding.tvContent.text = state.notes.joinToString("\n\n")
}
is NoteUiState.Error -> {
binding.progressBar.visibility = View.GONE
binding.tvContent.visibility = View.GONE
binding.tvError.visibility = View.VISIBLE
binding.tvError.text = state.message
}
}
}
}
}
运行验证
| 操作 | 预期结果 |
|---|---|
| 输入内容点击添加 | 列表自动刷新,显示新增的笔记 ✅ |
| 杀掉 App 重新打开 | 数据依然存在,持久化生效 ✅ |
| 旋转屏幕 | 数据不丢失,页面自动恢复状态 ✅ |
| 点击刷新 | 重新加载数据库数据 ✅ |
八、Room 编译时 SQL 检查的优势
这是 Room 相比原生 SQLite 最大的优势之一,值得单独强调:
| 对比维度 | 原生 SQLite(字符串 SQL) | Room(@Query 注解) |
|---|---|---|
| SQL 语法错误 | 运行时崩溃 ❌ | 编译期报错 ✅ |
| 表名/字段名拼写错误 | 运行时崩溃 ❌ | 编译期报错 ✅ |
| 返回类型不匹配 | 运行时崩溃 ❌ | 编译期报错 ✅ |
| 字段重构 | 手动检查所有 SQL | 自动检测 ✅ |
| IDE 支持 | 无语法高亮 | SQL 语法高亮 ✅ |
一个真实场景:假设你要将 createTime 重命名为 createdAt。在原生 SQLite 中,你需要手动搜索所有 SQL 字符串进行替换,很容易遗漏导致运行时崩溃。在 Room 中,只要修改了实体字段名,所有使用该字段的 @Query 都会在编译期报错,一个都不会漏。
九、FAQ / 避坑指南
Q1:Room 数据库为何必须设计为单例?
A:核心原因有三点:
| 原因 | 说明 | |
|---|---|---|
| 资源开销 | 数据库连接是重量级资源,频繁创建销毁会消耗大量内存与性能 | |
| 数据一致性 | 多实例并发操作会导致事务冲突、数据读写不一致,甚至数据库锁死 | |
| 事务安全 | 单例保证全局事务的统一管理,避免多实例下事务隔离失效 | . |
Room 官方文档也明确建议:一个应用只创建一个数据库实例。
Q2:Entity 实体类的定义规范有哪些?
A:遵循以下规范可以避免绝大多数 Room 开发问题:
| 规范 | 说明 |
|---|---|
| 优先使用 data class | 自动生成 equals、hashCode、toString |
| 主键必须明确 | 推荐使用 Long 类型的自增主键 |
| 字段可空性明确 | 非空字段用非空类型,Room 会在编译时校验 |
| 非存储字段加 @Ignore | 否则 Room 会尝试映射为表字段,编译报错 |
| 用 @ColumnInfo 显式命名 | 避免默认类名/属性名带来的兼容问题 |
Q3:Room 编译时 SQL 检查的优势是什么?
A:这是 Room 相比原生 SQLite 最大的工程化优势:
- 提前发现错误:SQL 语法错误、表名/字段名拼写错误、返回值类型不匹配,都在编译阶段直接报错
- 降低线上崩溃率:原生 SQLite 的 SQL 错误都是运行时异常,Room 把问题前置到开发阶段
- 重构更安全:修改表名、字段名时,DAO 中的 SQL 没有同步修改会直接编译报错
- 类型安全:查询返回的字段自动和实体类校验,避免运行时类型转换异常
Q4:ViewModel 为什么继承 AndroidViewModel?有什么注意事项?
A:因为 Repository 需要 Context 实例,而 AndroidViewModel 持有 Application Context,生命周期与 App 一致,不会造成内存泄漏。
注意:AndroidViewModel 只能访问 Application Context,不能访问 Activity Context。如果需要 Activity 级别的上下文(如显示 Dialog),应该通过 LiveData 或回调将操作委托给 Activity 执行。
十、迁移小结:从内存到数据库
| 阶段 | 数据存储方式 | 特点 |
|---|---|---|
| 第 2-3 篇 | ViewModel 中硬编码数据 | 演示用,无持久化 |
| 第 4 篇 | Repository 内存数据 | 架构完整,但重启丢失 |
| 本篇 | Room 数据库 | 持久化存储,重启不丢失 ✅ |
至此,我们的记事本数据已经真正"落地"了。App 关闭再打开,数据依然存在。同时,Repository 分层架构保证了数据源切换对上层完全透明——从模拟数据切换到 Room 数据库,ViewModel 和 UI 层一行代码都没有改动。
下篇预告
本篇我们从零搭建了 Room 数据库,掌握了实体、DAO、数据库三大核心组件,完成了记事本增删改查全功能的持久化实现,与 Repository 分层架构完美衔接。
但目前的查询还是一次性的,数据变化后需要手动调用刷新方法,还没有实现响应式自动更新。下一篇我们将进入 Room 进阶,讲解:
- 响应式查询:使用 LiveData/Flow 实现数据变更自动刷新列表
- 数据库迁移:版本升级时新增/删除字段的安全处理
- 复杂查询:多表联查、聚合查询、模糊搜索优化
🌟 点赞 + 收藏 + 关注,第一时间收到更新推送 💬 评论区聊聊:你在使用 Room 时踩过哪些数据库相关的坑?欢迎留言交流!