1. DataStore 的设计初衷与核心特点
为什么从 SharedPreferences(SP)升级到 DataStore?
-
异步 + Flow:基于协程与 Flow,避免 SP 的主线程 I/O 与回调不一致问题。
-
一致性与原子性:单写者模型,顺序一致、事务式更新(整份数据作为单元)。
-
类型安全(Proto) :通过 protobuf 定义 schema,避免字符串 key 易错。
-
可组合性:天然与 MVVM/Compose/Flow 打通,状态驱动 UI。
-
内建迁移 & 损坏处理:支持从 SP 迁移、文件损坏快速恢复。
核心理念:用一次性求解/事务更新替代“到处 setXXX”的键值散写,数据变化即 Flow 推送,UI 订阅即可。
2. 两种形态:Preferences vs Proto
| 形态 | 存储模型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| Preferences DataStore | 无 schema 的 key-value | 小量偏好设置、简单开关 | 接入快、API 类似 SP | key 仍是字符串,类型安全一般 |
| Proto DataStore | 有 schema 的自定义对象(protobuf) | 配置结构化、需版本演进 | 强类型、易演进、可读性强 | 需要 proto 文件与序列化器 |
3. Preferences DataStore 实战
3.1 依赖
implementation "androidx.datastore:datastore-preferences:1.1.1" // 版本示例
3.2 创建(建议 Application 级单例)
// 文件: SettingsDataStore.kt
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
✅ 用 applicationContext,不要在不同地方创建多个实例指向同一文件。
3.3 定义 key
private val KEY_DARK_MODE = booleanPreferencesKey("dark_mode")
private val KEY_USERNAME = stringPreferencesKey("username")
private val KEY_FONT_SIZE = intPreferencesKey("font_size")
3.4 写入(事务式)
suspend fun setDarkMode(context: Context, enabled: Boolean) {
context.settingsDataStore.edit { it[KEY_DARK_MODE] = enabled }
}
3.5 读取(Flow)
val darkModeFlow: Flow<Boolean> = context.settingsDataStore.data
.catch { e ->
if (e is IOException) emit(emptyPreferences()) else throw e
}
.map { it[KEY_DARK_MODE] ?: false }
3.6 Compose 集成
@Composable
fun DarkModeSwitch() {
val ctx = LocalContext.current
val dark by ctx.settingsDataStore.data
.map { it[KEY_DARK_MODE] ?: false }
.collectAsState(initial = false)
Switch(checked = dark, onCheckedChange = { enabled ->
LaunchedEffect(enabled) { ctx.settingsDataStore.edit { it[KEY_DARK_MODE] = enabled } }
})
}
4. Proto DataStore 实战(强烈推荐用于结构化配置)
4.1 依赖
implementation "androidx.datastore:datastore:1.1.1"
implementation "com.google.protobuf:protobuf-javalite:3.25.3"
并启用 protobuf Gradle 插件(省略完整配置,这里聚焦核心用法)。
4.2 定义 schema(.proto)
syntax = "proto3";
option java_package = "com.example.settings";
option java_multiple_files = true;
message UserSettings {
bool dark_mode = 1;
string username = 2;
int32 font_size = 3;
}
4.3 创建Serializer
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings =
try { UserSettings.parseFrom(input) }
catch (e: InvalidProtocolBufferException) { defaultValue }
override suspend fun writeTo(t: UserSettings, output: OutputStream) =
t.writeTo(output)
}
4.4 构建 DataStore(单例)
val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
4.5 读写
val settingsFlow: Flow<UserSettings> =
context.userSettingsDataStore.data
.catch { if (it is IOException) emit(UserSettings.getDefaultInstance()) else throw it }
suspend fun updateFontSize(context: Context, size: Int) {
context.userSettingsDataStore.updateData { cur ->
cur.toBuilder().setFontSize(size).build()
}
}
5. 从 SharedPreferences 迁移
Preferences DataStore 迁移:
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
produceMigrations = { ctx ->
listOf(SharedPreferencesMigration(ctx, "legacy_sp_name"))
}
)
Proto 迁移(自定义读取 SP 值,转为 proto):
val Context.userSettingsDataStore by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer,
produceMigrations = { ctx ->
listOf(
SharedPreferencesMigration(ctx, "legacy_sp_name") { sp, current ->
current.toBuilder()
.setDarkMode(sp.getBoolean("dark_mode", false))
.setUsername(sp.getString("username", "") ?: "")
.build()
}
)
}
)
6. 异常、文件损坏与恢复
- 读取异常:对 data Flow 使用 catch { if (e is IOException) emit(default/empty) else throw e }。
- 文件损坏恢复:ReplaceFileCorruptionHandler。
val store = DataStoreFactory.create(
serializer = UserSettingsSerializer,
corruptionHandler = ReplaceFileCorruptionHandler { UserSettings.getDefaultInstance() },
produceFile = { File(context.filesDir, "user_settings.pb") }
)
7. 线程、作用域、生命周期
- DataStore 内部管理 I/O,你无需切 dispatcher;但数据处理(map/filter)可能较重时,放 Dispatchers.Default/IO 更稳。
- 单例原则:同一文件仅创建一个实例。建议放在 DI 容器或 Context 扩展里。
- 不要跨进程复用同一文件(见 §10)。
8. 性能与实践建议
- 尽量使用 0..n 次写合并:在 ViewModel 层批量 edit/updateData,减少频繁 I/O。
- 小数据:DataStore 适合 KB 级配置;大数据或集合用 Room。
- 避免频繁订阅/取消订阅:将 Flow 复用在 Repository 层,UI 只 collect.
- 读路径去抖/去重:distinctUntilChanged() 避免无效重绘。
9. 测试(单元/仪器)
@get:Rule val tmp = TemporaryFolder()
@Test
fun proto_read_write() = runTest {
val scope = TestScope(UnconfinedTestDispatcher())
val store = DataStoreFactory.create(
serializer = UserSettingsSerializer,
scope = scope.backgroundScope,
produceFile = { tmp.newFile("user_settings.pb") }
)
store.updateData { it.toBuilder().setUsername("fan").build() }
val value = store.data.first()
assertEquals("fan", value.username)
}
10. 加密与敏感数据
-
DataStore 不自带加密。存敏感信息(token、私钥)建议:
- 用 Jetpack Security(EncryptedFile / EncryptedSharedPreferences)或
- 用 Tink 自行对 InputStream/OutputStream 加解密,再交给 Serializer。
-
或者:敏感信息走 Keystore + 短期内存持有,避免落盘。
11. 多进程与并发访问
- 默认定位:单进程使用。多个进程同时访问同一文件,可能带来不可见或竞争问题。
- 若确有多进程需求:通过 IPC(ContentProvider/Service/Binder) 暴露数据,或把写入集中到主进程代理;不要在各进程创建同名 DataStore 实例。
12. 与 SP / Room 的选型对比
| 场景 | 首选 |
|---|---|
| 简单偏好与开关 | Preferences DataStore |
| 结构化配置,需演进/类型安全 | Proto DataStore |
| 表格/关系型数据、查询/排序/分页 | Room(或数据库) |
| 强加密小数据 | EncryptedSharedPreferences / 自定义加密 + DataStore |
13. 常见坑与排查清单
- 多个实例指向同一文件 → 坚持单例。
- catch 忽略 IOException → 读取失败直接崩;按示例处理。
- 在 UI 层直接频繁 edit → 移到 VM/Repo 层做合并。
- proto 演进忘记默认值 → 新字段务必给默认,兼容旧文件。
- 滥用 wrap_content 心智(对布局而言)→ 和 DataStore 无关,但 UI 订阅频繁时注意 distinctUntilChanged。
- 多进程 → 不要这么用,改 IPC。
14. 参考范式(Repository 暴露强类型 Flow)
class SettingsRepo(private val ds: DataStore<UserSettings>) {
val darkMode: Flow<Boolean> = ds.data.map { it.darkMode }.distinctUntilChanged()
suspend fun setDarkMode(enabled: Boolean) {
ds.updateData { it.toBuilder().setDarkMode(enabled).build() }
}
}
UI:
@Composable
fun SettingsScreen(repo: SettingsRepo) {
val dark by repo.darkMode.collectAsState(initial = false)
Switch(checked = dark, onCheckedChange = { e ->
LaunchedEffect(e) { repo.setDarkMode(e) }
})
}
一句话总结
DataStore = 异步 Flow + 事务更新 +(可选)强类型 schema。偏好/配置用它几乎是“现代 Android”的默认答案:小数据首选 Preferences/Proto DataStore,复杂数据上数据库;注意单例、异常处理、迁移与(必要时)加密,多进程场景做 IPC。