跳到正文 Seminar Week 7 Summary · Jimmy Huang's Blog
← Week 7 Android Jetpack: Room 学习 › C6001 › 2026 S1 › FIT5046 Mobile and distributed computing systems › Week 7 Android Jetpack: Room Seminar Week 7 Summary Seminar Week 7 Summary 本页目录· 88 条 ChatGPT 说 › Learning Outcomes 理解 Android 应用中为什么需要本地数据持久化(Persistence)。
掌握 Room 持久化库(Room Persistence Library)的基本架构: 理解 Room 与 SQLite 的关系,以及 Room 为什么适合存储较小规模的本地结构化数据。
能够通过 Kotlin 符号处理(Kotlin Symbol Processing, KSP)配置 Room 所需的注解处理环境。
理解 Android 主线程(Main Thread)/ UI 线程(UI Thread)不能被阻塞的原因。
掌握 Kotlin 协程(Kotlin Coroutines)、挂起函数(Suspend Function)、调度器(Dispatcher)和协程作用域(Coroutine Scope)的基本作用。
理解流(Flow)如何把数据库中的连续更新转化为 UI 可观察的状态(State)。
本周在课程中的位置 本周内容从前几周的 Android UI 与 Jetpack Compose 进一步推进到 数据层(Data Layer) 和 异步编程(Asynchronous Programming) 。
可以这样理解:
本页目录 Room 与 Coroutines 被放在同一周讲,是因为二者在实际 Android 开发中经常一起使用:
Room 负责本地数据库读写。
数据库 I/O 通常较慢、不可预测,不能直接运行在主线程。
Kotlin Coroutines 用来把数据库操作放到后台线程执行,避免 UI 卡顿。
Flow 用来监听数据库变化,并把变化自动反映到界面状态中。
课堂中还强调:Assignment 2 / Assignment 4 中 Room 是必须使用的本地数据库方案。可以同时使用云端数据库或其他数据源,但 Room 仍然是必需实现项。
课堂补充与作业提醒
GitLab 协作要求 每个小组都有专门的 GitLab repository。
登录使用 Monash 认证信息。
master 或 main 分支应始终保存最终可评估版本。
评分时会查看官方 GitLab repository 和 commit history。
频繁、真实的 commit 记录可以体现个人贡献。
其他 GitHub / GitLab 仓库可以作为辅助,但评分主要看课程指定仓库。
AI Acknowledgement 提交作业时不要忘记 AI acknowledgement。
AI 交互记录应尽量使用英文。
如果之前已经用中文或其他语言与 AI 交流,后续至少应提供英文版本或英文翻译。
老师强调本课程是英文授课课程,作业材料原则上应使用英文,方便 TA 和审核人员理解。
Week 9 Quiz 提醒 Week 9 会有 in-class quiz。
形式为 1 小时、16 道选择题。
题型可能是单选或多选。
不需要现场写代码,但需要理解概念和代码逻辑。
Android Architecture:Room 放在哪一层
Android 应用的三层结构
和 MVC 的关系 课堂中提到,可以粗略类比传统的模型-视图-控制器(Model-View-Controller, MVC):
但现代 Android 架构并不是简单照搬 MVC,而是更强调:
状态管理;
数据流动;
生命周期;
异步安全;
UI 与数据层解耦。
Android Room:本地数据持久化
Room 是什么 Room 持久化库(Room Persistence Library)是 Android Jetpack 的一部分。
SQLite 提供底层的关系型数据库管理系统(Relational Database Management System, RDBMS)。
Room 提供更安全、更清晰、更适合 Android/Kotlin 的访问方式。
开发者不需要直接写大量底层 SQLite 代码。
Room 会通过注解和编译期检查减少运行时错误。
SQLite 是底层数据库,Room 是 Android 为开发者准备的更好用的数据库访问框架。
Room 适合存什么数据 本地数据;
小规模结构化数据;
需要在 App 关闭后仍然保留的数据;
用户个人设备上的私有数据;
不需要立即跨设备共享的数据。
Room 不适合直接承担所有云端同步需求。如果数据需要跨用户、跨设备共享,可能还需要结合:
课堂中强调:Assignment 中可以根据设计选择额外数据源,但 Room 是必须实现的本地数据库方案。
Room and Kotlin KSP
为什么 Room 需要 KSP Room 使用大量注解(Annotations),例如:
@Entity
@Dao
@Database
@Insert
@Update
@Delete
@Query
这些注解需要在编译阶段被处理。KSP(Kotlin Symbol Processing)就是用来处理 Kotlin 注解的工具。
Kotlin 编译时读取 Room 注解,并自动生成数据库相关代码的工具。 如果没有正确配置 KSP,Room 的注解通常无法被正确识别,项目可能无法编译。
KSP 与 Room 的基本配置 在 libs.versions.toml 中声明 KSP:
在 project-level build.gradle.kts 中声明插件:
在 module-level build.gradle.kts 中启用插件并添加依赖:
Room Architecture:核心组件总览
Entity:定义数据库表
Entity 是什么 每个 @Entity class 对应数据库中的一张 table。
class 的属性默认对应 table 的 column。
class 的一个对象实例对应 table 中的一行数据。
如果没有指定表名,Room 默认使用 class name 作为 table name。
如果需要自定义表名,可以使用 @Entity(tableName = "...")。
如果需要自定义列名,可以使用 @ColumnInfo(name = "...")。
如何识别 Entity 课堂中提到,Entity 通常是系统中需要长期保存的“名词”。
Student
Subject
Unit
Tutor
Assignment
Subject Entity 示例 课件中有 Subject 示例。需要注意:不同示例中出现过 Subject 和 subjects 两种表名写法,实际项目中表名必须保持一致。下面统一使用 subjects:
自定义列名示例
DAO:定义数据库操作
DAO 是什么 数据访问对象(Data Access Object, DAO)定义如何访问数据库中的数据。
SubjectDao 示例 insertSubject、 updateSubject、 deleteSubject 是 suspend fun,因为数据库写操作不应该阻塞 UI 线程。
getAllSubjects() 返回 Flow<List<Subject>>,表示它不是只返回一次数据,而是可以持续监听数据库变化。
当数据库中 subjects 表变化时,Flow 可以继续发出新的列表数据。
@Query 的特点 如果表名写错;
如果 column name 不存在;
如果 SQL 和 Entity 不匹配;
Query 参数写法 如果 query 中需要传入参数,要在 SQL 字符串中使用冒号 ::
Database:创建 Room 数据库实例
Database 类的基本要求 数据库类(Database)负责创建和管理 Room database instance。
必须是 RoomDatabase 的 abstract subclass。
必须使用 @Database 注解。
@Database 中要列出所有 Entity。
要指定数据库版本 version。
通常要指定 exportSchema。
必须提供一个无参数 abstract function,用来返回 DAO。
通常使用单例模式(Singleton Pattern)确保整个 App 只有一个 database instance。
SubjectDatabase 示例
@Database 注解中的参数
version 新增 Entity;
删除 Entity;
修改字段;
修改表结构;
exportSchema exportSchema 默认为 true,表示 Room 会导出数据库 schema,用于版本控制和迁移。
原因是课程示例不需要复杂的 versioning 或 migration。
Migration:数据库迁移
什么是 Migration 迁移(Migration)指的是数据库结构从旧版本升级到新版本的过程。
fallbackToDestructiveMigration 如果没有提供 migration;
并且数据库版本发生变化;
Room 可以直接删除旧表并重新创建表。
如果数据必须保留,就不能随便 destructive migration,而应该实现自动或手动 migration。
Companion Object:为什么要用单例
单例模式的目的 课堂中强调,Room database 通常应该只有一个 instance。
多个 database instance 同时打开可能导致资源浪费;
多线程同时创建 database 可能导致不一致;
整个 App 共享同一个 database instance 更安全。
companion object 的作用 companion object 用于在 Kotlin 中实现类似 static 的共享成员。
@Volatile 和 synchronized @Volatile 负责“看得见最新值”, synchronized 负责“同一时间只让一个线程创建”。
Repository:封装数据访问
Repository 是什么 存储库(Repository)是数据层中的抽象层。
课堂中强调,Repository 是本周 Android 架构中比较新的抽象层概念。
DAO 负责“具体怎么操作数据库”,Repository 负责“对外提供统一、干净、安全的数据接口”。
SubjectRepository 示例
Repository 的关键点 Constructor 接收 Application,用于创建或获取 database instance。
Repository 获取 DAO instance。
Repository 中的方法通常是 DAO 方法的 wrapper。
DAO 的 suspend function 在 Repository 中也保持 suspend。
allSubjects 继续以 Flow 暴露,方便 ViewModel 和 UI 监听变化。
ViewModel:连接 UI 与 Repository
ViewModel 是什么 视图模型(ViewModel)是 UI 层中的状态持有者(State Holder)。
ViewModel 不直接画 UI,也不直接操作数据库;它负责把数据准备好、管理好,再交给 UI 使用。
为什么这里使用 AndroidViewModel 课件示例使用的是 AndroidViewModel,不是普通 ViewModel。
Repository 需要 Application 参数;
AndroidViewModel 的 constructor 可以接收 Application;
因此可以把 Application 传给 Repository。
SubjectViewModel 示例
ViewModel 中的关键点 viewModelScope 是 ViewModel 自带的 coroutine scope。
Dispatchers.IO 表示这些数据库操作应在 I/O 线程池中执行。
launch {} 用于启动一个新的 coroutine。
insert / update / delete 不会阻塞主线程。
allSubjects 作为 Flow 暴露给 UI,用于实时更新界面。
Room + ViewModel + UI 的完整数据流 用户在 UI 中点击 Add button。
Composable 调用 ViewModel 的 insertSubject()。
ViewModel 使用 viewModelScope.launch(Dispatchers.IO) 启动后台 coroutine。
ViewModel 调用 Repository 的 insert()。
Repository 调用 DAO 的 insertSubject()。
DAO 通过 Room 把数据写入 SQLite。
数据库变化后, Flow<List<Subject>> 发出新数据。
UI 收集 Flow,并转换为 Compose State。
Compose 自动重新渲染界面,显示最新 subject list。
Android 主线程模型
Android 的 Single Thread Model 当 Android App 启动时,系统会创建一个主线程(Main Thread),也叫 UI 线程(UI Thread)。
处理用户输入;
更新界面;
执行 UI toolkit 相关操作;
运行 Compose UI 的部分逻辑。
两条核心规则
Rule 1:不要阻塞主线程 数据库访问;
网络请求;
文件系统访问;
大规模计算;
任何耗时不可预测的任务。
Rule 2:不要在非 UI 线程直接访问 Android UI toolkit 耗时任务不要放主线程,UI 操作不要随便放后台线程。
Background Work:后台工作的类型 课件把 Android 中的后台相关任务分成三类:
Room 数据库操作属于典型的 I/O 操作,因此应使用 coroutine 放到后台线程处理。
Kotlin Coroutines:异步编程方案
Coroutine 是什么 协程(Coroutine)是一种并发设计模式,用来简化异步编程。
管理长时间运行的任务;
避免阻塞 UI 线程;
让异步代码写起来接近普通顺序代码;
减少传统 callback 写法的复杂度。
直接阻塞主线程:不好,会卡 UI;
写 callback:可行但复杂、容易混乱;
使用 coroutine:更清晰、更现代。
suspend function 挂起函数(Suspend Function)用于表示可以被 coroutine 挂起和恢复的函数。
暂停当前 coroutine;
保存当前局部变量和执行状态;
让线程可以去做别的工作;
等待结果回来后继续执行。
suspend 是“暂停 coroutine”,不是“卡住 thread”。
Coroutine 与 Thread 的区别
launch 与 suspend 的关系
launch launch {} 用于启动一个新的 coroutine。
suspend function 调用规则 因此 ViewModel 不能直接在普通函数中调用 Repository 的 suspend function,而是要放进:
Dispatchers:决定 coroutine 运行在哪类线程上 调度器(Dispatcher)决定 coroutine 应该在哪类线程上执行。
Dispatchers.IO 原因是数据库访问属于 I/O 操作,不应该占用主线程。
Dispatchers.Main 但在 Jetpack Compose 中,常见方式不是手动切回 Main 更新 UI,而是:
withContext 示例
Coroutine Scopes:控制 coroutine 生命周期 协程作用域(Coroutine Scope)决定 coroutine 的生命周期和取消规则。
Flow:连续数据流
为什么需要 Flow 普通 suspend function 通常返回一个单一结果。
数据库表内容变化;
传感器持续产生数据;
网络状态变化;
UI 需要持续观察某个状态。
Flow 是什么 Flow 是基于 coroutines 的异步数据流。
suspend function 返回单个值;Flow 可以连续发出多个值。
Room 中的 Flow
Repository 中继续暴露 Flow Repository 不改变 Flow 的本质,只是把 DAO 的数据流封装后提供给 ViewModel。
ViewModel 中暴露 Flow
Compose 中 collectAsState
Flow 示例:连续传感器数据 课件最后用 sensor data 说明 Flow 也可以表示连续数据流。
Data Provider:生成连续数据 flow { ... } 创建一个 Flow;
emit(randomValue) 发出一个值;
delay(1000) 每秒等待一次;
while (true) 表示持续产生数据。
ViewModel:私有可变,公开只读
UI:收集 StateFlow 并显示 当 ViewModel 中的 sensor data 更新时,UI 会自动更新显示。
Android Studio 中查看 Room 数据 打开 Android Studio。
找到右侧工具栏中的 App Inspector。
选择 Database Inspector。
运行 App 后,可以看到当前 App 的本地数据库。
展开 database,可以查看其中的 table。
双击 table,可以查看当前运行时保存的数据。
这对于检查 Room 是否真的保存了数据很有帮助。
本周核心代码结构复习 一个最小 Room + Coroutine + ViewModel 项目通常包含:
配置 KSP 和 Room dependencies。
创建 Entity。
创建 DAO。
创建 Database。
创建 Repository。
创建 ViewModel。
在 UI 中 collect Flow。
通过 Database Inspector 验证数据是否保存。
常见错误与复习重点
1. 表名不一致 默认表名可能是 class name,例如 Subject。
2. 忘记 suspend
3. 在主线程做数据库操作
4. UI 直接访问 DAO
5. 忽视 migration 如果改了数据库结构但没有处理 migration,可能导致:
课堂示例可以用 fallbackToDestructiveMigration(true),但正式项目要谨慎。
6. 滥用 GlobalScope 因为它不自动跟随 ViewModel 或 Activity 生命周期取消,容易造成后台任务失控。
Summary 本周的核心是把 Android App 从“只有界面和临时状态”推进到“有本地持久化数据和异步数据流”。
Room 负责把结构化数据安全地保存到本地 SQLite 数据库中。Entity 定义表,DAO 定义数据库操作,Database 创建数据库实例,Repository 封装数据访问,ViewModel 把数据暴露给 UI。
Kotlin Coroutines 解决的是数据库、网络、文件系统等慢速 I/O 不能阻塞主线程的问题。通过 suspend、 viewModelScope.launch(Dispatchers.IO) 和合适的 dispatcher,可以让数据库操作在后台执行,同时保持 UI 流畅。
Flow 是 Room 与 Compose UI 之间的重要桥梁。它可以持续监听数据库变化,并通过 collectAsState() 转换成 Compose State,让 UI 自动随着数据更新而重组。
Room 负责“数据保存在哪里”,Coroutines 负责“慢操作怎么不阻塞 UI”,Flow 负责“数据变化如何自动更新界面”。 [versions]
ksp = "2.0.21-1.0.26"
[plugins]
ksp = {id = "com.google.devtools.ksp",version.ref = "ksp" }UI / Composable
↓
ViewModel
↓
Repository
↓
DAO
↓
Room Database
↓
SQLite table / EntityUser action
↓
Composable
↓
ViewModel
↓
Repository
↓
DAO
↓
Room Database
↓
SQLite
↓
Flow emits updated data
↓
Compose State updates UIRoom table changes
↓
Flow emits new List<Subject>
↓
collectAsState converts Flow to State
↓
Compose recomposes UISubject.kt // Entity
SubjectDao.kt // DAO
SubjectDatabase.kt // RoomDatabase + Singleton
SubjectRepository.kt // Repository wrapper
SubjectViewModel.kt // UI state holder + coroutine launcher
MainActivity.kt // Composable UI + collectAsStateUI → ViewModel → Repository → DAOplugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.ksp) apply false } plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) } dependencies { implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) } import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "subjects" ) data class Subject( @PrimaryKey(autoGenerate = true) val uid: Int = 0, val name: String ) import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "users" ) data class User( @PrimaryKey val uid: Int, @ColumnInfo(name = "first_name" ) val firstName: String?, @ColumnInfo(name = "last_name" ) val lastName: String ) import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Update import kotlinx.coroutines.flow.Flow @Dao interface SubjectDao { @Query( "SELECT * FROM subjects" ) fun getAllSubjects(): Flow<List<Subject>> @Insert suspend fun insertSubject(subject: Subject) @Update suspend fun updateSubject(subject: Subject) @Delete suspend fun deleteSubject(subject: Subject) } @Query( "SELECT * FROM subjects WHERE uid = :subjectId" ) fun findById(subjectId: Int): Flow<Subject?> import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @Database( entities = [Subject::class], version = 1, exportSchema = false ) abstract class SubjectDatabase : RoomDatabase() { abstract fun subjectDao(): SubjectDao companion object { @Volatile private var INSTANCE: SubjectDatabase? = null fun getDatabase(context: Context): SubjectDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, SubjectDatabase::class.java, "subject_database" ) .fallbackToDestructiveMigration(true) .build() INSTANCE = instance instance } } } } .fallbackToDestructiveMigration(true) @Volatile private var INSTANCE: SubjectDatabase? = null synchronized(this) { // create database instance } import android.app.Application import kotlinx.coroutines.flow.Flow class SubjectRepository(application: Application) { private val subjectDao: SubjectDao = SubjectDatabase.getDatabase(application).subjectDao() val allSubjects: Flow<List<Subject>> = subjectDao.getAllSubjects() suspend fun insert(subject: Subject) { subjectDao.insertSubject(subject) } suspend fun delete(subject: Subject) { subjectDao.deleteSubject(subject) } suspend fun update(subject: Subject) { subjectDao.updateSubject(subject) } } import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch class SubjectViewModel(application: Application) : AndroidViewModel(application) { private val cRepository: SubjectRepository init { cRepository = SubjectRepository(application) } val allSubjects: Flow<List<Subject>> = cRepository.allSubjects fun insertSubject(subject: Subject) = viewModelScope.launch(Dispatchers.IO) { cRepository.insert(subject) } fun updateSubject(subject: Subject) = viewModelScope.launch(Dispatchers.IO) { cRepository.update(subject) } fun deleteSubject(subject: Subject) = viewModelScope.launch(Dispatchers.IO) { cRepository.delete(subject) } } @Update suspend fun updateSubject(subject: Subject) fun updateSubject(subject: Subject) = viewModelScope.launch(Dispatchers.IO) { cRepository.update(subject) } viewModelScope.launch(Dispatchers.IO) { cRepository.insert(subject) } suspend fun loadData() = withContext(Dispatchers.IO) { // database or network operation } suspend fun insertSubject(subject: Subject) @Query( "SELECT * FROM subjects" ) fun getAllSubjects(): Flow<List<Subject>> val allSubjects: Flow<List<Subject>> = subjectDao.getAllSubjects() val allSubjects: Flow<List<Subject>> = cRepository.allSubjects val subjects by subjectViewModel.allSubjects.collectAsState( initial = emptyList() ) fun sensorDataFlow(): Flow<Int> = flow { while (true) { val randomValue = Random.nextInt(5, 40) emit(randomValue) delay(1000) } } private val _sensorData = MutableStateFlow(0) val sensorData: StateFlow<Int> = _sensorData.asStateFlow() val data by viewModel.sensorData.collectAsState() Text( text = "Sensor value:$data" ) @Entity(tableName = "subjects" ) @Query( "SELECT * FROM subjects" ) suspend fun insertSubject(subject: Subject) viewModelScope.launch(Dispatchers.IO) { repository.insert(subject) }