DataStore

65 阅读4分钟

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 类似 SPkey 仍是字符串,类型安全一般
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. 常见坑与排查清单

  1. 多个实例指向同一文件 → 坚持单例。
  2. catch 忽略 IOException → 读取失败直接崩;按示例处理。
  3. 在 UI 层直接频繁 edit → 移到 VM/Repo 层做合并。
  4. proto 演进忘记默认值 → 新字段务必给默认,兼容旧文件。
  5. 滥用 wrap_content 心智(对布局而言)→ 和 DataStore 无关,但 UI 订阅频繁时注意 distinctUntilChanged。
  6. 多进程 → 不要这么用,改 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。