Room 与 DataStore 简介
这一章我们来聊聊 Android 本地存储里两个最常用的家伙——Room 和 DataStore。说白了,它们都是帮你在设备上存数据的东西,但各自的脾气秉性完全不同。我见过不少团队在这两个库之间反复横跳,花了一堆时间最后还是没选对。嗯,今天我就把我的经验掰开揉碎讲给你听。
Room:关系型数据库的老牌玩家
Room 是 Google 官方出的 ORM 框架,底层就是 SQLite。你想想看,SQLite 在移动端已经跑了十几年了,稳定性不用怀疑。Room 帮你把 SQL 表映射成 Kotlin/Java 对象,省去了写大量样板代码的麻烦。
核心特点:强关系、事务性、查询灵活(支持 SQL)、编译时校验。
我个人习惯用 Room 来处理有复杂关联关系的数据,比如订单+商品明细、用户+收藏列表这类场景。举个例子:你在电商 App 里看到的“我的订单”页面,订单表和商品表有一对多关系,用 Room 的 @Relation 注解可以一句查询就拉出来。
@Entity(tableName = "orders")
data class Order(
@PrimaryKey val id: Long,
val customerId: Long,
val totalAmount: Double,
val status: String
)
@Entity(tableName = "order_items")
data class OrderItem(
@PrimaryKey val id: Long,
val orderId: Long,
val productName: String,
val quantity: Int
)
data class OrderWithItems(
@Embedded val order: Order,
@Relation(
parentColumn = "id",
entityColumn = "orderId"
)
val items: List<OrderItem>
)
@Dao
interface OrderDao {
@Transaction
@Query("SELECT * FROM orders WHERE id = :orderId")
suspend fun getOrderWithItems(orderId: Long): OrderWithItems
}
避坑注意:我曾经在项目里把联表查询写成了 N+1 问题——Room 默认的懒加载会逐条拉数据,性能直接崩。后来加了个 @Transaction 才解决。记住,凡是涉及嵌套对象查询,务必挂上这个注解。
Room 的另一个优势是支持 增量迁移。什么意思?就是你的数据库表结构变了,Room 可以自动帮你执行迁移脚本,不至于丢数据。我接手过一个老项目,SQLite 迁移全靠手写 onUpgrade(),每次上线都要手动跑脚本,现在换了 Room,省心多了。
DataStore:轻量级键值对的新贵
DataStore 是 SharedPreferences 的替代品。你问为什么谷歌要换掉 SharedPreferences?那玩意儿同步读写会卡 UI、没有类型安全、还容易引发 ANR。DataStore 一上来就用 Kotlin 协程和 Flow,天生就是异步的。
DataStore 分两种:Preferences DataStore 和 Proto DataStore。
| 类型 | 存储形式 | 适用场景 | 类型安全 |
|---|---|---|---|
| Preferences DataStore | 键值对(可 null) | 简单配置、用户偏好 | 否(key 用字符串) |
| Proto DataStore | Protocol Buffers 序列化 | 需要结构化的少量数据 | 是(生成 Kotlin 类) |
我建议:如果你的 App 只需要存几十个布尔值或字符串,比如“是否已阅读条款”“通知开关”,直接用 Preferences 就行。但如果要存“用户的主题配色方案”(包含主色、辅色、字体大小)这种稍微结构化的东西,Proto 更合适。
一致性保障:两条路完全不同
这里要敲黑板了。Room 和 DataStore 对“一致性”的理解不太一样。
- Room 用 SQLite 的事务保证 ACID。你写一条数据,要么全部成功要么全部失败,不存在中间状态。这在离线场景下太重要了——比如用户离线时提交了一条订单,Room 可以保证订单头和明细同时写入,不会出现“有订单无商品”的脏数据。
- DataStore 不提供多键原子性。它保存和读取都是“整体快照”——你写入时传入一个
Transform函数,函数内部可以修改多个 key,但整个写入是原子性的。不过注意,它没有跨记录的事务概念。
个人经验:我做过一个离线笔记 App,用户离线时创建笔记和标签。如果用 DataStore 存标签列表,笔记内容用 Room 存,就得自己搞一套分布式事务——别提多坑了。后来我把笔记和标签全放 Room 里,用一个事务搞定。DataStore 只存用户设置和缓存 ,这才安全。
选型快照:谁该上场?
为了让你一眼看清楚,我整理了一个对比表格:
| 维度 | Room | DataStore |
|---|---|---|
| 数据模型 | 关系型(表 + 关系) | 键值对 / 结构化对象 |
| 数据量 | 几十万条没问题 | 建议几百条以内 |
| 事务支持 | ACID 事务,跨表 | 单次写入原子,无跨键事务 |
| 同步方式 | Flow + 协程,支持监听变更 | Flow 推,协程写 |
| 迁移能力 | 版本号 + 迁移脚本 | 无(销毁重写或 Json 迁移) |
| 启动速度 | 略慢(Schema 建立) | 极快 |
| 典型场景 | 离线数据缓存、业务实体 | 用户偏好、配置、轻量缓存 |
你想想看,如果你的需求是“离线优先”的——比如用户在没网时也能创建数据,等有网了再同步——那我建议用 Room 做主存储,因为它的事务能力和查询灵活性是 DataStore 给不了的。DataStore 更多是用来存“状态”而非“业务实体”。
一句话总结:需要 CRUD + 关系 + 离线写入一致性 → Room;需要简单配置 + 快速读写 + 少量数据 → DataStore。别混用,但可以共存。
最后聊一句我的真实感受。有一回我们团队用 DataStore 存离线待办列表,结果用户同时写两条待办时,出现了部分丢失——因为 DataStore 的写入是合并式,不是追加式。后来我全都迁到了 Room,用 @Insert(onConflict = REPLACE) 解决问题。所以说,选型不能只看“简单”,数据一致性在离线场景里永远是第一位的。