当前位置: 首页 > news >正文

Room 与 Flow:本地数据源现代写法

文章目录

  • 第 1 章 裸 SQLite 的痛点:为什么用 Room
      • 原理补充
      • 踩坑
      • 动手练
  • 第 2 章 Entity、DAO、Database:三层骨架
      • 踩坑
      • 动手练
  • 第 3 章 suspend:一次性读写与事务
      • 原理补充
      • 踩坑
      • 动手练
  • 第 4 章 Flow:可观察查询与 UI 联动
      • 原理补充
      • 踩坑
      • 动手练
  • 第 5 章 关系、迁移与多表
      • 踩坑
      • 动手练
  • 第 6 章 Repository 组合:网络 + 本地单一数据源
      • 动手练
  • 面试速查 · 追问链
      • 追问链 #1:Room 和裸 SQLite 本质区别? 🔥
      • 追问链 #2:suspend 和 Flow 在 DAO 里怎么选? 🔥
      • 追问链 #3:@Transaction 用在哪? ⭐
      • 追问链 #4:Migration 生产注意什么? ⭐
      • 追问链 #5:Repository 为何要以 Room 为真相源? 💡
  • 完整链路一句通
  • 相关推荐

第 1 章 裸 SQLite 的痛点:为什么用 Room

手写SQLiteOpenHelper+ SQL 字符串,在 Java 工程里常见三类问题:

  1. 编译期无校验:列名拼错运行时才发现。
  2. 映射样板Cursor逐列getString,DTO 与表结构容易漂移。
  3. 观察困难:数据变了要手动notifyDataSetChanged,与 UI 生命周期脱节。

Room 是 Jetpack 官方 ORM:编译期验证 SQL、生成DAO实现、与Kotlin 协程 / Flow一等集成。新本地库默认 Room,裸 SQLite 仅作遗留对照。

@Entity(tableName="orders")dataclassOrderEntity(@PrimaryKeyvalid:String,valtitle:String,valstatus:String,valupdatedAt:Long,)@DaointerfaceOrderDao{@Query("SELECT * FROM orders ORDER BY updatedAt DESC")funobserveAll():Flow<List<OrderEntity>>@Query("SELECT * FROM orders WHERE id = :id")suspendfungetById(id:String):OrderEntity?@Insert(onConflict=OnConflictStrategy.REPLACE)suspendfunupsertAll(orders:List<OrderEntity>)}

原理补充

Room 在编译期通过注解处理器生成_Impl类;@Query在编译期绑定参数与返回类型。运行时由RoomDatabase管理连接池与InvalidationTracker

踩坑

  • 把网络 DTO 直接当@Entity→ 领域模型与表结构耦合,应分Entity+Mapper
  • 主线程查询 → 默认禁止;用suspendFlow在后台调度。
  • 无版本迁移直接改表 → 线上崩溃;必须Migration或开发期fallbackToDestructiveMigration(仅 debug)。

动手练

  • A:用一句话说明 Room 相对裸 SQLite 的两个编译期优势。
  • D:搜索所在工程@Database,列出version与是否定义Migration

第 2 章 Entity、DAO、Database:三层骨架

Entity映射表;DAO声明访问;Database聚合 DAO 与版本。

@Database(entities=[OrderEntity::class],version=1,exportSchema=true,)abstractclassAppDatabase:RoomDatabase(){abstractfunorderDao():OrderDao}// 模块级单例(或由 Hilt 提供)funbuildDatabase(context:Context):AppDatabase=Room.databaseBuilder(context.applicationContext,AppDatabase::class.java,"app.db",).addMigrations(MIGRATION_1_2)// 生产必备.build()

类型转换器处理 Room 不直接支持的类型:

classConverters{@TypeConverterfunfromStatus(s:OrderStatus):String=s.name@TypeConverterfuntoStatus(raw:String):OrderStatus=OrderStatus.valueOf(raw)}

索引与外键在 Entity 上声明,查询计划可预测:

@Entity(tableName="orders",indices=[Index(value=["status"])],)dataclassOrderEntity(/* ... */)

踩坑

  • data class主构造属性才映射列;类体var默认不参与。
  • @PrimaryKey(autoGenerate = true)Stringid 策略要想清楚,避免与服务端 id 冲突。
  • exportSchema = false省事但团队难审查表结构变更。

动手练

  • B(起点):为BookmarkEntity(id, url, title)@Entity+ 带OnConflictStrategy.REPLACE@Insert(≤12 行)。

第 3 章 suspend:一次性读写与事务

「拉一次、写一次」用suspend,Room 自动在后台线程执行(勿在主线程调)。

@DaointerfaceOrderDao{@TransactionsuspendfunreplaceAll(orders:List<OrderEntity>){clearAll()upsertAll(orders)}@Query("DELETE FROM orders")suspendfunclearAll()@Query("SELECT COUNT(*) FROM orders WHERE status = :status")suspendfuncountByStatus(status:String):Int}

@Transaction保证多步操作原子性;挂起函数里可调用同 DAO 其它suspend方法。

Repository 网络刷新后写库:

classOrderRepository(privatevalapi:OrderApi,privatevaldao:OrderDao,privatevalmapper:OrderMapper,){suspendfunrefreshOrders(){valdtos=api.fetchOrders()valentities=dtos.map(mapper::toEntity)dao.upsertAll(entities)}}

原理补充

Room 2.x 的suspendCoroutineRoom包装,在RoomDatabase配置的Executor或协程上下文执行。@Transaction方法必须是suspend或同步,且同线程嵌套事务。

踩坑

  • @Transaction里开新协程 → 事务边界被打断。
  • suspend fun里调非 suspend 的阻塞 IO 未切线程 → 仍可能卡默认调度器。
  • 忘记OnConflictStrategy→ 主键冲突插入失败。

动手练

  • C:写refreshAndCount()upsertAllreturn countByStatus("OPEN")@Transaction)。
  • A@Transaction解决什么问题?一句话。

第 4 章 Flow:可观察查询与 UI 联动

列表、详情等「数据变 UI 自动变」用Flow。Room 在表变更时通过InvalidationTracker重新执行查询并发射新列表。

@DaointerfaceOrderDao{@Query("SELECT * FROM orders WHERE status = :status ORDER BY updatedAt DESC")funobserveByStatus(status:String):Flow<List<OrderEntity>>}

ViewModel 收集并映射为 UiState:

classOrderListViewModel(privatevalrepository:OrderRepository,):ViewModel(){privateval_filter=MutableStateFlow(OrderStatus.ALL)valuiState:StateFlow<OrderListUiState>=_filter.flatMapLatest{status->repository.observeOrders(status).map{orders->if(orders.isEmpty())OrderListUiState.EmptyelseOrderListUiState.Success(orders)}}.stateIn(scope=viewModelScope,started=SharingStarted.WhileSubscribed(5_000),initialValue=OrderListUiState.Loading,)funonFilterChanged(status:OrderStatus){_filter.value=status}}

flatMapLatest在筛选切换时取消旧订阅,避免竞态覆盖。stateIn把冷流提升为热StateFlow供 UI 收集。

原理补充

Flow查询是冷流:每个收集者触发查询;InvalidationTracker监听表 invalidation 后emit。多表 JOIN 时任一表变都会刷新。distinctUntilChanged可在map后去重。

踩坑

  • 在 DAO 返回Flow却期望「只查一次」→ 应用suspend
  • 忘记stateIn/shareIn导致每次collect重复订阅(视场景而定)。
  • Flow映射很重时不加flowOn(Dispatchers.Default)→ 主线程压力。

动手练

  • B(起点):下面在主线程collect冷 Flow 写库,指出问题并改 Repository 层:
funbroken()=dao.observeAll()// 直接暴露 DAO

第 5 章 关系、迁移与多表

一对一 / 一对多@Relation+@Embedded,或显式 JOIN:

dataclassOrderWithLines(@Embeddedvalorder:OrderEntity,@Relation(parentColumn="id",entityColumn="orderId")vallines:List<LineEntity>,)@Query("SELECT * FROM orders WHERE id = :id")suspendfungetOrderWithLines(id:String):OrderWithLines?

迁移生产必做:

valMIGRATION_1_2=object:Migration(1,2){overridefunmigrate(db:SupportSQLiteDatabase){db.execSQL("ALTER TABLE orders ADD COLUMN note TEXT NOT NULL DEFAULT ''")}}

开发可fallbackToDestructiveMigration(dropAllTables = true)禁止对线上用户默认开启。

踩坑

  • @Relation查询 N+1 性能问题 → 大列表改 JOIN 或分页(Paging 篇)。
  • 迁移 SQL 写错无单元测试覆盖 → 用MigrationTestHelper
  • 多模块共用一个AppDatabase→ 注意 DAO 暴露边界与模块依赖方向。

动手练

  • AexportSchema = true对 Code Review 有何帮助?
  • D:为现有表设计1→2加列迁移 SQL 一行。

第 6 章 Repository 组合:网络 + 本地单一数据源

现代模式:Room 为 UI 真相源;网络刷新写库;UI 只观察 Room 的Flow

classOrderRepository(privatevalapi:OrderApi,privatevaldao:OrderDao,privatevalmapper:OrderMapper,){funobserveOrders(status:OrderStatus):Flow<List<Order>>=dao.observeByStatus(status.name).map{entities->entities.map(mapper::toDomain)}suspendfunrefresh(status:OrderStatus){valremote=api.fetchOrders(status.apiParam)dao.upsertAll(remote.map(mapper::toEntity))}}

离线优先:先展示本地Flow,并行refresh();失败时保留旧数据并暴露错误态(ViewModel 合并网络错误与本地流)。

与 Paging 3 结合时,Room 作PagingSourceRemoteMediator的缓存层(见 Paging 篇)。

反模式改法
UI 直接调 DAO经 Repository
网络成功才写内存列表写 Room + observe Flow
全表SELECT *无分页Paging / LIMIT

动手练

  • C:实现observeOrders+refresh完整 Repository(≤20 行)。
  • D 验收:断网打开列表应显示上次缓存;恢复网络下拉刷新后 Flow 自动更新。

面试速查 · 追问链

追问链 #1:Room 和裸 SQLite 本质区别? 🔥

标准回答(≤200 字):Room 编译期验证 SQL 与返回类型,注解生成实现,减少 Cursor 样板。与协程suspend、KotlinFlow集成,自带线程调度约束。迁移、关系、FTS 有官方模式。新工程默认 Room;裸 SQLite 仅遗留或极特殊场景。

追问 1:SQL 写错何时发现?
:编译期@Query校验;运行时 migration 错误需测试兜底。

追问 2:主线程能查 Room 吗?
:默认不能;suspend/Flow在后台执行,UI 用 ViewModel 收集。


追问链 #2:suspend 和 Flow 在 DAO 里怎么选? 🔥

标准回答(≤200 字):一次性读/写用suspend;需要数据变就推送 UI 用Flow。列表页、购物车数量等「观察」场景用Flow。单次拉详情、事务批量写用suspend。不要对「只查一次」误用Flow

追问 1Flow查询如何感知表变化?
InvalidationTracker监听表 invalidation,自动 re-query emit。

追问 2:多个收集者会怎样?
:冷流各自订阅;应用层常用stateIn/shareIn转热流。


追问链 #3:@Transaction 用在哪? ⭐

标准回答(≤200 字):多步写操作要原子性时用:先删后插、多表联动。@Transaction方法内调同 DAO 其它方法在同一数据库事务。避免在事务里再launch新协程。

追问 1:和 SQLite 手动beginTransaction关系?
:Room 生成的代码封装了 begin/setTransactionSuccessful/end。


追问链 #4:Migration 生产注意什么? ⭐

标准回答(≤200 字):升version必须提供Migration或破坏性降级策略(仅开发)。exportSchema存档备查。测试用MigrationTestHelper跑真 SQL。加列注意DEFAULT兼容旧行。

追问 1:能否线上fallbackToDestructiveMigration
:等于清用户数据,一般禁止;仅 debug 或可丢数据场景。


追问链 #5:Repository 为何要以 Room 为真相源? 💡

标准回答(≤200 字):UI 观察本地Flow,旋转/进程恢复后仍能展示;网络刷新写库,自动推送。离线可用、逻辑单一。网络层不直接喂 Adapter,避免双份状态不同步。

追问 1:与 Paging 如何配合?
PagingSource读 Room;RemoteMediator网络分页写库。


完整链路一句通

Entity/DAO/Database 建模suspend 写、Flow 观察Repository 网络刷新 upsertViewModelflatMapLatest+stateIn迁移与@Transaction保一致


相关推荐

Kotlin 语法与空安全:Android 开发第一课

Kotlin 作用域函数:let/apply 工程选型

http://www.cnnetsun.cn/news/3568025.html

相关文章:

  • 读了1000行老代码,我发现最好的设计是“简单”
  • Mac微信插件开发终极指南:10个提升开发效率的工具集成技巧
  • React Native ECharts社区贡献指南:如何为开源项目贡献力量
  • 5分钟快速上手:用Karabiner-Elements彻底改造你的macOS键盘体验
  • 【AI工作流效率翻倍的7个隐藏杠杆】:20年资深架构师首次公开内部SOP清单
  • Narratium.ai:三步开启你的AI角色扮演奇幻之旅
  • 如何让90%抗拒AI的工程师主动拥抱工具?揭秘高成功率团队推广的5步黄金法则
  • Ingress2Gateway 与 Gateway API 版本兼容性:从 v1.0 到 v1.5 的演进指南
  • 从零实现C++高效JSON解析器:词法分析、递归下降与性能优化实战
  • command-line-args插件系统扩展:如何自定义类型转换器与验证器 [特殊字符]
  • 挖掘波兰采购客户核心三大技巧
  • Electron Vite Monorepo架构终极指南:pnpm Workspaces + Turborepo实战
  • 自动售货机库存上报异常物联网卡DNS解析失败修复教程
  • 原神抽卡数据分析终极指南:用开源工具genshin-wish-export轻松掌握你的抽卡命运
  • 如何快速集成weapp.socket.io?3分钟上手微信小程序WebSocket开发
  • 自然语言处理(NLP)学习资源:Data-Science-EBooks NLP书籍推荐终极指南
  • Jetpack Compose布局实战:Why-Not-Compose中的响应式设计案例
  • 为什么选择MicroG?华为设备用户的终极GMS替代方案完整指南
  • nebula.gl部署指南:生产环境配置与性能监控
  • eDBG性能优化:如何高效调试Android系统库(libc.so、libart.so)
  • Agent 原理通识:从概念到核心工作机制
  • dotnet-packaging架构深度解析:理解打包工具的内部工作原理
  • AI工具小白入门组合:20年IT老兵的“最小可行组合”公式——仅需2工具+1规则,效率提升300%
  • 小程序毕业设计-基于 SpringBoot 的校园设备报修与互助服务小程序 高校宿舍报修与校园互助平台的设计与实现(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • TiDB In Action进阶教程:Titan与TiFlash深度优化实战
  • 开源游戏开发新选择:Solarus引擎核心功能与优势解析
  • 视频通用模型来了!何恺明等新作GenCeption:训练量仅1/500,精度持平SOTA!
  • 毕业设计项目 深度学习Yolo11暴力行为识别系统(源码+论文)
  • Dex Retargeting项目深度解析:如何利用Python优化器实现精准的手部运动重定向
  • 小程序毕业设计-基于 SpringBoot 的高校宿舍防疫与日常管理系统 智慧校园宿舍防疫管控服务小程序设计(源码+LW+部署文档+全bao+远程调试+代码讲解等)