第三章 Preferences DataStore 基础API与数据读写

本章聚焦 Preferences DataStore 的核心编程模型。你将掌握如何通过委托属性或工厂方法创建 DataStore 实例,如何进行同步/异步的读写操作,如何利用 Kotlin Flow 观察数据变化,以及如何正确处理文件读写中的常见异常。这些 API 是后续所有离线缓存迁移技术的基础。

3.1 依赖引入与项目配置

在 build.gradle.kts(模块级)中添加如下依赖:

dependencies {
    // Preferences DataStore
    implementation("androidx.datastore:datastore-preferences:1.1.1")
    // 可选:如果使用 RxJava2 扩展
    // implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")
    // 可选:如果使用 RxJava3 扩展
    // implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
}

同步后即可使用 Preferences DataStore 的全部 API。

模块artifact版本
核心datastore-preferences1.1.1
RxJava2datastore-preferences-rxjava21.1.1
RxJava3datastore-preferences-rxjava31.1.1

3.2 创建 DataStore 实例

DataStore 实例推荐通过 Kotlin 顶层扩展属性创建,保证全局单例。标准做法是在文件顶部使用 preferencesDataStore 委托:

// 顶层扩展属性,全局唯一
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")

参数 name 指定文件名(实际文件为 user_prefs.preferences_pb)。若需要自定义序列化管理器或指定不同文件路径,可使用 DataStoreFactory:

val dataStore = DataStoreFactory.create(
    serializer = PreferencesSerializer(),
    produceFile = { context.filesDir.resolve("my_custom.prefs") }
)
提示: 始终将 DataStore 实例声明为 private val 或 internal,避免在多个地方重复创建导致文件竞争。

3.3 写入数据:edit 与 transform

写入操作必须在协程作用域中调用,通常使用 DataStore.edit 挂起函数:

suspend fun saveUserName(name: String) {
    context.dataStore.edit { preferences ->
        preferences[USER_NAME_KEY] = name
    }
}

private val USER_NAME_KEY = stringPreferencesKey("user_name")

如果需要原子性条件写入(先读取再决定是否写入),可使用 DataStore.transform 返回 Flow 并配合 collect 实现;但标准场景推荐 edit 配合 Preferences 的 MutablePreferences 操作。

  • stringPreferencesKey — 存储 String 类型
  • intPreferencesKey — 存储 Int 类型
  • booleanPreferencesKey — 存储 Boolean 类型
  • floatPreferencesKey — 存储 Float 类型
  • longPreferencesKey — 存储 Long 类型
  • doublePreferencesKey — 存储 Double 类型

所有 Preferences.Key<T> 必须定义为顶层 val 以避免重复创建。

3.4 读取数据:data Flow 与瞬时值

DataStore.data 属性返回 Flow<Preferences>,每次文件变更时自动发射新值:

val userNameFlow: Flow<String?> = context.dataStore.data
    .map { preferences ->
        preferences[USER_NAME_KEY]
    }

// 在 ViewModel 或 Compose 中收集
viewModelScope.launch {
    context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences()) // 恢复默认空值
            } else {
                throw exception
            }
        }
        .map { it[USER_NAME_KEY] ?: "默认用户" }
        .collect { name ->
            // 更新 UI
        }
}

如果需要获取一次性的当前值(非 Flow),可以使用 DataStore.data.first():

val currentName = context.dataStore.data.first()[USER_NAME_KEY]
注意: .first() 是挂起函数,不要在主线中直接调用。DataStore 的读取是异步的,调用 .first() 会等待文件就绪。

3.5 异常处理策略

DataStore 的文件 I/O 可能抛出 IOException(如磁盘满、文件损坏)。推荐在 data Flow 中使用 catch 操作符,并在 edit 块使用 try-catch:

suspend fun safeWrite(key: Preferences.Key<String>, value: String) {
    try {
        context.dataStore.edit { prefs ->
            prefs[key] = value
        }
    } catch (e: IOException) {
        // 记录日志,可考虑重试或回退
        Log.e("DataStore", "写入失败", e)
    }
}

对于 Flow 读取,推荐模式:

context.dataStore.data
    .catch { e ->
        if (e is IOException) {
            Log.w("DataStore", "读取异常,使用空 Preferences", e)
            emit(emptyPreferences())
        } else {
            throw e
        }
    }
    .map { preferences -> preferences[KEY] }
    .collect { value -> /* 使用 value */ }

3.6 完整示例:读写与观察

一个简洁的 Repository 风格实现:

class UserPreferencesRepository(private val context: Context) {
    private val dataStore = context.dataStore

    companion object {
        private val KEY_NICKNAME = stringPreferencesKey("nickname")
        private val KEY_AGE = intPreferencesKey("age")
    }

    val nicknameFlow: Flow<String> = dataStore.data
        .catch { emit(emptyPreferences()) }
        .map { it[KEY_NICKNAME] ?: "未知" }

    suspend fun updateNickname(nickname: String) {
        dataStore.edit { prefs ->
            prefs[KEY_NICKNAME] = nickname
        }
    }

    suspend fun clearAll() {
        dataStore.edit { it.clear() }
    }
}

3.7 流程图:Preferences DataStore 基础操作流程

Preferences DataStore 基础操作流程 创建 DataStore 实例 写入(edit) 读取(data Flow) 异常处理 (IOException) Flow 观察 & 变换 catch 恢复默认 UI 更新 持续 Flow 观察 ⚠ 重点说明: • edit 是挂起函数,确保在协程中调用;catch 操作符必须放在 map 之前以捕获文件异常。 • Flow 观察默认在主线程收集,建议通过 .flowOn(Dispatchers.IO) 调整上游线程。

3.8 常见错误与调试

错误现象可能原因解决方案
写入后读取为旧值未使用同一 DataStore 实例或 key 定义不一致确保 key 是顶层 val 且使用相同 name 创建实例
java.io.IOException: Operation not permitted写入时磁盘权限不足或文件被锁定检查存储权限(Android 10+ 无需额外权限),确保不在主线程调用 edit
Flow 不发射新值未在 edit 中提交变更,或重复使用了 DataStore.data.first() 阻塞确认 edit 块内修改了指定 key;使用 catch 处理异常后重新发射
应用重启后数据丢失文件损坏或未等待写入完成确保在 Kill 前调用过至少一次 edit 并等待完成;可添加 flush() 调试

3.9 性能与稳定性建议

  • 最小化写入频率: 高频写入(如每 100ms)会导致大量序列化与磁盘 IO,应使用去抖动(debounce)合并操作。
  • 避免在 edit 中做复杂计算: transform 块会持有写锁,耗时操作会阻塞其他读写。
  • 使用 Dispatchers.IO 调度 Flow: 默认 DataStore.data 的读取在 Dispatchers.IO,但 .map 后的操作应显式指定调度器。
  • 适当选择 first() vs collect: 仅需当前值用 first(),持续监听用 collect,避免内存泄漏。
调试建议: 在开发阶段可以通过 DataStore.data 的 cachedSnapshot 调试属性查看当前缓存值(需 API 版本 1.1.0+)。或在 Logcat 中过滤 DataStore 标签观察文件操作日志。