【Jetpack 内核实战 05 】:Room 入门——实体 / DAO / 数据库搭建与增删改查

16 阅读14分钟

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/toStringdata 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 时踩过哪些数据库相关的坑?欢迎留言交流!