news 2026/7/21 19:42:19

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 工程选型

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/21 19:41:45

读了1000行老代码,我发现最好的设计是“简单”

读了1000行老代码&#xff0c;我发现最好的设计是“简单” 摘要&#xff1a; 运维多年&#xff0c;第一次用AI深读WMS库位管理模块。1000多行代码&#xff0c;AI几分钟就扫完了&#xff0c;但真正让我震撼的&#xff0c;是一个跑了10多年的蛇形算法——奇数排从左到右&#x…

作者头像 李华
网站建设 2026/7/21 19:40:49

Mac微信插件开发终极指南:10个提升开发效率的工具集成技巧

Mac微信插件开发终极指南&#xff1a;10个提升开发效率的工具集成技巧 Mac微信插件开发为开发者提供了丰富的功能扩展能力&#xff0c;通过WeChatExtension-ForMac项目&#xff0c;开发者可以为Mac版微信添加消息防撤回、自动回复、皮肤主题、多开登录等实用功能。本指南将分享…

作者头像 李华
网站建设 2026/7/21 19:36:20

React Native ECharts社区贡献指南:如何为开源项目贡献力量

React Native ECharts社区贡献指南&#xff1a;如何为开源项目贡献力量 【免费下载链接】react-native-echarts Echarts for react-native. The react-naitve chart. 项目地址: https://gitcode.com/gh_mirrors/re/react-native-echarts React Native ECharts是一个专为…

作者头像 李华
网站建设 2026/7/21 19:35:04

5分钟快速上手:用Karabiner-Elements彻底改造你的macOS键盘体验

5分钟快速上手&#xff1a;用Karabiner-Elements彻底改造你的macOS键盘体验 【免费下载链接】Karabiner-Elements Karabiner-Elements is a powerful tool for customizing keyboards on macOS 项目地址: https://gitcode.com/gh_mirrors/ka/Karabiner-Elements 你是否曾…

作者头像 李华
网站建设 2026/7/21 19:32:20

Narratium.ai:三步开启你的AI角色扮演奇幻之旅

Narratium.ai&#xff1a;三步开启你的AI角色扮演奇幻之旅 【免费下载链接】AI-Chat Open-source platform for AI-driven storytelling, worldbuilding, and immersive roleplay 项目地址: https://gitcode.com/gh_mirrors/na/AI-Chat 你是否曾幻想过与AI角色进行深度对…

作者头像 李华