跳到正文Lab做完总结 · Jimmy Huang's BlogLab做完总结
本页目录· 109 条
这一页有部分内容暂时无法显示,其余内容完整。
1. 一句话总结这个 Lab
这个 Lab 教会你把一个 Jetpack Compose App 从“只在界面里临时显示数据”的小程序,升级成一个真正会把数据存进手机本地数据库、并且能随着数据库变化自动刷新 UI 的 CRUD App。
2. 这个 Lab 实际做了什么
这次 Lab 的主线可以理解为:
用户在界面上新增、修改、删除 subject
此处有一段内容暂不支持显示。
按照页面可见内容,本 Lab 主要做了以下事情:
2.1 创建一个 Compose 项目
你会在 Android Studio 里创建一个新的 Jetpack Compose 项目,页面可见内容建议项目名可以是:
这个项目一开始只是一个普通的 Compose App,还没有真正的本地数据库能力。
2.2 配置 Room 与 KSP 环境
你需要在项目里配置:
Room runtime
Room KTX
Room Compiler
KSP 插件
为什么要做这一步?
因为后面会使用 Room 的注解,例如:
这些注解不是普通 Kotlin 自己能直接“理解并生成代码”的。KSP 会在编译阶段帮你根据这些注解生成 Room 需要的底层代码。
如果这一步没配好,后面的 Entity、DAO、Database 可能会报错,或者 Room 无法生成数据库相关代码。
2.3 定义 Entity:告诉 Room 数据表长什么样
本 Lab 的数据对象是 subject。你需要创建一个类似 Subject 的 Entity。
具体字段以 Lab 代码为准;如果页面未显示完整字段,则字段细节属于“材料未显示/不确定”。
2.4 定义 DAO:告诉 Room 怎么查、增、改、删
DAO 是 Data Access Object,也就是“数据访问对象”。
查询所有 subjects
新增 subject
修改 subject
删除 subject
@Query
@Insert
@Update
@Delete
把“我要操作数据库”这件事,封装成一组 Kotlin 方法,而不是让 UI 到处写 SQL。
2.5 定义 Database:把 Entity 和 DAO 组装成数据库入口
Database 类会使用 @Database 注解,告诉 Room:
这个数据库包含哪些 Entity
当前数据库版本是多少
可以通过哪个 DAO 访问数据
Entity 是表结构,DAO 是操作方法,Database 是整个数据库的大门。
没有 Database 类,Room 不知道你的数据库有哪些表,也不知道应该怎么拿到 DAO。
2.6 加入 Repository:让数据层更清楚
为什么不让 ViewModel 直接调用 DAO?
因为 Repository 可以把数据来源封装起来,让 ViewModel 不需要关心底层细节。
现在数据可能来自 Room 本地数据库;以后真实项目里,数据也可能来自:
本地 Room 数据库
远程 API
缓存
Firebase / Firestore
文件系统
Repository 的作用就是让上层代码不用知道这些细节。
2.7 加入 ViewModel:让 UI 不直接碰数据库
Compose UI 不应该直接操作 Room 数据库,因为这样会导致:
UI 代码太复杂
数据库逻辑和界面逻辑混在一起
难测试
数据变化后不好自动刷新
容易在主线程做数据库操作,导致卡顿
ViewModel 负责接收 UI 的意图,例如:
用户点击 Add
用户点击 Update
用户点击 Delete
页面需要显示 subject 列表
然后 ViewModel 再调用 Repository 完成真正的数据操作。
2.8 使用 Flow:让数据库变化自动通知 UI
页面可见内容强调:这周要理解为什么 Room 常常和 Flow / suspend function 一起用,以及为什么 Compose UI 会随着数据库变化自动刷新。
UI 不是自己偷偷改列表,而是数据库变了以后,Flow 把新数据推回 UI,Compose 再自动刷新。
这不是因为你手动刷新了整个页面,而是因为数据流自动把变化推了回来。
2.9 用 LazyColumn 显示 subject 列表
页面可见内容提到,需要把 Room 的 Flow 数据接到 Compose 的 LazyColumn。
LazyColumn 可以理解为 Compose 里的滚动列表。
它负责把数据库里的 subject 一条一条显示出来。
当 Flow 发出新的列表时,Compose 收到新的 State,LazyColumn 就会重新显示最新数据。
2.10 完成 subject 的新增、修改、删除
本 Lab 的最终功能是一个小型 CRUD App。
Create:新增
Read:读取
Update:修改
Delete:删除
新增一个 subject
读取并显示所有 subjects
修改某个 subject
删除某个 subject
页面可见内容提到要验证数据确实写进本地数据库。具体用什么工具验证,页面没有完整显示;在 Android 开发中常见做法是使用 Android Studio 的 Database Inspector,但如果原 Lab 没显示这个步骤,就应标注为:材料未显示/不确定。
3. 每个核心组件有什么用
3.1 Room
它是什么?
Room 是 Android 官方推荐的本地数据库封装库。它底层仍然使用 SQLite,但比直接写 SQLite 更安全、更清晰、更适合现代 Android 项目。
它在这个 Lab 里负责什么?
Room 负责把 Kotlin 代码里的 Entity、DAO、Database 连接到真正的 SQLite 数据库。
它让你不用自己写大量底层 SQLite 管理代码。
没有它会发生什么?
没有 Room,你可能需要直接使用 SQLiteOpenHelper 或原生 SQLite API。这样代码会更长、更容易出错,也不容易和 Flow、协程、Compose UI 结合。
它和其他组件有什么关系?
DAO 告诉 Room 要做什么,Room 再去操作底层 SQLite。
3.2 SQLite
它是什么?
SQLite 是手机本地真正存储结构化数据的数据库。
它在这个 Lab 里负责什么?
它负责把 subject 数据真正保存到设备本地。
没有它会发生什么?
如果没有本地数据库,数据可能只保存在内存里。App 一关掉,subject 数据就可能丢失。
它和 Room 有什么关系?
Room 是外层更好用的接口,SQLite 是底层真正存数据的数据库。
你大多数时候通过 Room 间接使用 SQLite,而不是直接操作 SQLite。
3.3 Entity
它是什么?
Entity 是数据库表结构在 Kotlin 里的表示。
它在这个 Lab 里负责什么?
Subject Entity 用来定义 subject 表里一条记录长什么样。
没有它会发生什么?
Room 不知道数据库里要创建什么表,也不知道每条数据有哪些字段。
它和其他组件有什么关系?
Entity 会被 Database 引用,DAO 操作的也是 Entity 对象。
3.4 DAO
它是什么?
DAO 是 Data Access Object,中文可以理解为“数据访问对象”。
它在这个 Lab 里负责什么?
查询所有 subjects
新增一个 subject
更新一个 subject
删除一个 subject
没有它会发生什么?
没有 DAO,Repository 或 ViewModel 就不知道怎么访问数据库。
它和其他组件有什么关系?
DAO 被 Repository 调用,DAO 再通过 Room 操作 SQLite。
3.5 Database
它是什么?
它在这个 Lab 里负责什么?
它把 Entity 和 DAO 连接起来,告诉 Room:
没有它会发生什么?
Room 没有统一入口,就不知道怎么创建数据库实例,也不知道这个数据库包含哪些 Entity。
它和其他组件有什么关系?
Database 同时知道 Entity 和 DAO:
3.6 Repository
它是什么?
它在这个 Lab 里负责什么?
Repository 把 DAO 包起来,给 ViewModel 提供更干净的数据操作接口。
没有它会发生什么?
ViewModel 可能会直接调用 DAO。短期看能跑,但项目一大就会变乱。
比如以后你想把数据来源从 Room 改成远程 API,ViewModel 就可能要大改。
它和其他组件有什么关系?
Repository 位于 ViewModel 和 DAO 之间:
3.7 ViewModel / AndroidViewModel
它是什么?
ViewModel 是负责管理 UI 数据和业务操作的组件。
AndroidViewModel 是带有 Application context 的 ViewModel 变体,在需要 Application 级别资源时可能会用到。
它在这个 Lab 里负责什么?
没有它会发生什么?
UI 可能会直接操作 Repository、DAO 或 Database,导致界面层和数据层混在一起。
它和其他组件有什么关系?
ViewModel 是 UI 和数据层之间的中转站:
3.8 Compose UI / SubjectScreen
它是什么?
Compose UI 是用 Kotlin 写界面的方式。SubjectScreen 可以理解为本 Lab 中显示 subject 列表和操作按钮的页面。
它在这个 Lab 里负责什么?
显示输入框
显示按钮
显示 subject 列表
响应用户点击
把用户操作交给 ViewModel
没有它会发生什么?
用户看不到数据,也没法点击 Add / Update / Delete。
它和其他组件有什么关系?
Compose UI 不应该直接访问数据库,而应该通过 ViewModel 间接访问:
3.9 LazyColumn
它是什么?
LazyColumn 是 Compose 里的滚动列表组件。
它在这个 Lab 里负责什么?
它负责把 subject 数据一条一条显示在屏幕上。
没有它会发生什么?
你仍然可以显示数据,但如果列表很多,用普通 Column 会不够高效,也不适合长列表。
它和其他组件有什么关系?
LazyColumn 显示的是 Compose State 中的 subject 列表,而这个 State 通常来自 Flow。
3.10 Flow
它是什么?
Flow 是 Kotlin Coroutines 里的异步数据流。
它在这个 Lab 里负责什么?
DAO 查询 subject 列表时,可以返回 Flow<List<Subject>>。
当数据库内容变化时,Flow 会发出新的列表,UI 就能自动拿到最新数据。
没有它会发生什么?
你可能需要手动重新查询数据库、手动刷新列表。这样代码更麻烦,也容易忘记刷新。
它和其他组件有什么关系?
Flow 通常从 DAO 出来,经由 Repository 和 ViewModel,最后被 Compose UI 收集。
3.11 suspend function
它是什么?
suspend 函数是 Kotlin 协程里可以“暂停等待”的函数。
它在这个 Lab 里负责什么?
新增、修改、删除数据库数据通常会写成 suspend 函数,因为数据库操作不应该直接阻塞主线程。
没有它会发生什么?
如果耗时数据库操作直接跑在主线程,App 可能卡顿,严重时会出现 ANR。
它和 Flow 有什么关系?
3.12 collectAsState()
它是什么?
collectAsState() 是 Compose 中把 Flow 转成 Compose State 的工具。
它在这个 Lab 里负责什么?
它让 Compose UI 能订阅 ViewModel 提供的数据流。
当 Flow 发出新数据,Compose State 改变,界面自动刷新。
没有它会发生什么?
Flow 虽然能发数据,但 Compose UI 不知道如何把这个数据变成可触发重组的 State。
它和 Flow 有什么关系?
3.13 copy()
它是什么?
copy() 是 Kotlin data class 的常用方法。
它在这个 Lab 里负责什么?
当你修改一个 subject 时,可以用 copy() 基于原对象创建一个“改过字段的新对象”。
没有它会发生什么?
你仍然可以手动创建新对象,但代码会更啰嗦,也更容易漏字段。
它和 Update 有什么关系?
Update 通常需要一个带有原 id 的新对象。copy() 可以保留原 id,只改你要改的字段。
3.14 KSP
它是什么?
KSP 是 Kotlin Symbol Processing,是 Kotlin 项目里常用的代码生成工具。
它在这个 Lab 里负责什么?
Room 需要根据 @Entity、@Dao、@Database 等注解生成底层代码,KSP 就是负责辅助生成这些代码的工具。
没有它会发生什么?
Room Compiler 无法正常处理注解,项目可能编译失败。
它和 Room 有什么关系?
Room 使用 KSP 在编译阶段生成数据库相关代码。
4. 用初学者类比解释
可以把这个 Lab 想象成一个“学校课程档案管理系统”。
4.1 整体类比
你做的 App 就像一个学校教务处系统,用来管理 subjects。
4.2 为什么不能让 UI 直接碰数据库?
如果 Compose UI 直接操作数据库,就像:
教务处前台窗口的工作人员自己跑进档案仓库翻档案、改档案、锁柜子、写登记表。
前台会很忙,界面容易卡住。
前台代码会越来越乱。
以后换数据库或加网络 API 时,前台代码要大改。
很难测试。
数据变化后不容易统一通知界面刷新。
4.3 为什么数据库变了 UI 会自动刷新?
当 SQLite 里的 subject 表变了,Room/DAO 会通过 Flow 发出新的列表。
Compose UI 通过 collectAsState() 一直监听这个 Flow。
5. 重点区分容易混淆的概念
5.1 Room 和 SQLite 的区别
Room 不是另一个完全独立的数据库。Room 的底层还是 SQLite。
你平时写代码主要接触 Room,是因为 Room 更适合 Android 开发。
5.2 Entity 和 Database 的区别
Database 是 Room 的总入口,负责把 Entity 和 DAO 组装起来。
5.3 DAO 和 Repository 的区别
短期小项目里 Repository 看起来像“多一层”,但真实项目中很有用。
5.4 ViewModel 和 UI 的区别
ViewModel 是“当前页面有什么数据、用户操作后应该发生什么”。
5.5 suspend 和 Flow 的区别
5.6 Flow 和 collectAsState() 的区别
必须通过 collectAsState() 这类方式,Compose 才能根据新数据自动刷新。
5.7 入口到底是谁?
5.8 谁能访问谁?
数据可以通过 Flow “回流”到 UI,但不是数据库直接操作 UI。
5.9 哪个是权限控制?哪个是网络控制?
根据页面可见内容,本 Lab 主要是 Android 本地数据库和 Compose UI,不是云网络或安全权限 Lab。
Room / DAO / Entity:不是网络控制
ViewModel / Repository:不是权限控制
Flow / collectAsState():不是权限控制
SQLite:不是服务器数据库,也不是云数据库
如果 Lab 里没有涉及登录、用户权限、网络访问控制、防火墙、API 鉴权,那这些内容就不应强行加入。
本地数据持久化 + 分层架构 + 响应式 UI 刷新。
6. 这个 Lab 有什么实际用途
这个 Lab 非常实用,因为很多真实 App 都需要本地数据库。
6.1 做离线可用的 App
课程表 App
Todo List
记账 App
笔记 App
食物库存 App
购物清单 App
6.2 做缓存
6.3 支持 MVVM / Clean Architecture
这个 Lab 的结构非常接近真实 Android 项目常用分层:
这就是 Android 开发里常见的 MVVM 思路。
UI 更干净
数据层更清楚
更容易测试
更容易维护
以后接 API 或 Firebase 更方便
6.4 避免 App 卡顿
数据库操作如果直接跑在主线程,用户界面可能会卡住。
通过 Coroutines 和 suspend,可以把数据库操作放到异步流程里。
6.5 实现响应式 UI
6.6 对你后续作业的帮助
如果你后续做 Android 项目,比如 FreshGuard 这种需要本地草稿、食物列表、提醒、缓存的 App,这个 Lab 的结构可以直接迁移:
本地保存用户添加的食物
保存未提交的 listing 草稿
离线查看已经加载过的数据
用 Flow 自动刷新统计页面
删除过期食物后列表自动更新
7. 最后画一张生动形象的示意图
如果你在支持 Mermaid 的 Markdown 编辑器、Notion、GitHub 或部分 VS Code 插件里打开,可以看到流程图。
图里最重要的理解
7.1 用户操作线
7.2 数据刷新线
最后复习:这个 Lab 真正要你学会什么?
这个 Lab 真正想训练的是一种 Android 开发思维:
如果你能把下面这条链路讲清楚,就说明你已经理解了本 Lab 的核心:
ViewModel → Repository → DAO → Room Database
Compose UI → ViewModel → Repository → DAO → Room
Room 数据库里的 subject 表发生变化
→ DAO 返回的 Flow 发出新的 List<Subject>
→ ViewModel 持有这个数据流
→ Compose 用 collectAsState() 收集这个 Flow
→ Flow 变成 Compose State
→ State 变了,Compose 自动 recomposition
→ LazyColumn 列表自动刷新
Entity 定义表结构
DAO 操作 Entity
Database 注册 Entity
Room 根据 Entity 创建表
Repository → DAO → Room → SQLite
Database
├── Entity:表结构
└── DAO:操作方法
ViewModel → Repository → DAO
Compose UI → ViewModel → Repository
Compose UI → ViewModel → Repository → DAO → Room
DAO Flow → Repository → ViewModel → Compose collectAsState()
Flow<List<Subject>>
→ collectAsState()
→ State<List<Subject>>
→ Compose UI 自动刷新
前台窗口 → 前台主管 → 流程协调员 → 档案管理员 → 档案馆 → 仓库
Compose UI → ViewModel → Repository → DAO → Room → SQLite
档案仓库更新了
→ 广播系统 Flow 发通知
→ 前台喇叭 collectAsState() 收到
→ 前台窗口 Compose 自动换成最新显示
Compose UI
↓
ViewModel
↓
Repository
↓
DAO
↓
Room Database
↓
SQLite
Compose UI → SQLite
Compose UI → DAO
DAO → UI
Database → UI
SQLite → UI
数据库变化 → Flow 发出新数据 → ViewModel 暴露数据 → UI 收集数据 → UI 刷新
API 数据 → Room 本地缓存 → Compose UI 显示
UI → ViewModel → Repository → DAO → Database
FoodListing Entity
FoodListingDao
AppDatabase
FoodRepository
FoodViewModel
Compose Screen
flowchart TD
T["📱 Lab Week 7:Room + Compose 本地数据库 CRUD"]:::title
U["👤 用户<br/>点击 Add / Update / Delete"]:::user
UI["🖼️ Compose UI / SubjectScreen<br/>显示输入框、按钮、Subject 列表"]:::ui
VM["🧠 ViewModel / AndroidViewModel<br/>保存 UI 状态,接收用户操作"]:::logic
REPO["📦 Repository<br/>统一数据访问入口,隔离数据来源"]:::logic
DAO["🧾 DAO<br/>定义 insert / update / delete / query"]:::data
ROOM["🏛️ Room Database<br/>数据库大门:连接 Entity 和 DAO"]:::data
SQLITE["🗄️ SQLite 本地数据库<br/>真正保存 subject 数据"]:::store
ENTITY["📋 Entity:Subject<br/>定义 subject 表结构"]:::entity
FLOW["📡 Flow<List<Subject>><br/>数据库变化后发出新列表"]:::flow
CAS["🔄 collectAsState()<br/>把 Flow 转成 Compose State"]:::flow
REC["✨ Compose Recomposition<br/>State 改变后 UI 自动刷新"]:::ui
BAD1["❌ 错误路径:UI 直接操作 SQLite<br/>强耦合、难测试、容易卡 UI"]:::blocked
BAD2["❌ 阻塞路径:主线程直接做数据库耗时操作<br/>界面卡顿,严重时 ANR"]:::blocked
MAIN["🚦 Main Thread 主线程<br/>应该优先负责界面响应"]:::note
BG["🛠️ Coroutines / suspend<br/>把数据库操作放到异步流程"]:::note
CORE["✅ 你要记住的核心区别<br/>Entity 定义表;DAO 定义怎么操作表;Database 是入口;Repository 隔离数据层;ViewModel 连接数据和 UI;Compose UI 只负责显示;Flow 把数据库变化推回界面。"]:::summary
T --> U
U --> UI
UI --> VM
VM --> REPO
REPO --> DAO
DAO --> ROOM
ENTITY --> ROOM
ROOM --> SQLITE
SQLITE --> FLOW
FLOW --> CAS
CAS --> REC
REC --> UI
UI -. "❌ 不推荐直接访问" .-> BAD1
MAIN -. "❌ 不做耗时 DB 操作" .-> BAD2
BG --> DAO
SQLITE --> CORE
classDef title fill:#e8f2ff,stroke:#1d4ed8,stroke-width:2px,color:#0f172a;
classDef user fill:#fff7ed,stroke:#f97316,stroke-width:2px,color:#7c2d12;
classDef ui fill:#ecfdf5,stroke:#16a34a,stroke-width:2px,color:#064e3b;
classDef logic fill:#f5f3ff,stroke:#7c3aed,stroke-width:2px,color:#3b0764;
classDef data fill:#fefce8,stroke:#ca8a04,stroke-width:2px,color:#713f12;
classDef store fill:#f1f5f9,stroke:#475569,stroke-width:2px,color:#0f172a;
classDef entity fill:#e0f2fe,stroke:#0284c7,stroke-width:2px,color:#0c4a6e;
classDef flow fill:#ecfeff,stroke:#0891b2,stroke-width:2px,color:#164e63;
classDef blocked fill:#fee2e2,stroke:#dc2626,stroke-width:3px,color:#7f1d1d;
classDef note fill:#fafafa,stroke:#737373,stroke-width:1.5px,color:#171717;
classDef summary fill:#dcfce7,stroke:#15803d,stroke-width:3px,color:#14532d;
用户点击按钮
→ Compose UI
→ ViewModel
→ Repository
→ DAO
→ Room Database
→ SQLite
SQLite 数据变化
→ Flow 发出新列表
→ collectAsState() 收集
→ Compose State 改变
→ UI 自动刷新
Entity → DAO → Database → Repository → ViewModel → Compose UI
SQLite 数据变化 → Flow → collectAsState() → Compose Recomposition → UI 自动刷新
val updatedSubject = oldSubject.copy(name = "New Name")