DataStore:替代 SharedPreferences 的异步存储
DataStore 本地存储完全指南
为什么需要 DataStore?
在 Android 开发中,轻量级键值对存储过去一直依赖 SharedPreferences。虽然它简单易用,但在实际项目里会暴露出不少问题:
- 同步阻塞:commit/apply 操作可能导致主线程卡顿或 ANR。
- 数据不一致:缺少事务保护,多进程或多线程并发写入可能丢失数据。
- 无类型安全:存取值需要手动转换,容易发生运行时类型错误。
- 错误处理能力弱:读写异常通常只能捕获 RuntimeException。
- 不支持异步流:无法方便地观察数据变化。
Jetpack DataStore 正是为了解决上述痛点而设计的异步、一致性、类型安全的存储方案。本教程将带你从零掌握 DataStore 的使用。
DataStore 的两种实现
Preferences DataStore
类似 SharedPreferences 的键值对存储,使用键定义的方式存取数据。不需要预定义模式,直接使用 preferencesKey 声明键名。适用于简单设置、用户偏好等。
Proto DataStore
基于 Protocol Buffers 的强类型存储。需要定义 .proto 文件,生成相应的数据类,编译器会保证类型安全。适合存储复杂对象或结构化数据。
我们首先聚焦最常用的 Preferences DataStore,再简要介绍 Proto DataStore 的搭建方式。
环境准备
添加依赖
在模块级 build.gradle.kts 中加入:
dependencies {
// Preferences DataStore
implementation("androidx.datastore:datastore-preferences:1.1.1")
// 可选 - 配合协程使用
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
如果要使用 Proto DataStore,额外添加:
implementation("androidx.datastore:datastore:1.1.1")
implementation("com.google.protobuf:protobuf-kotlin-lite:3.24.4")
同步项目即可。
Preferences DataStore 快速上手
创建 DataStore 实例
不在 Activity 或 Fragment 中直接实例化,推荐使用属性委托创建单例(例如在 Context 扩展函数中):
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
// 扩展属性,确保整个应用只创建一个实例
val Context.dataStore by preferencesDataStore(name = "user_settings")
// 定义键名
object PreferenceKeys {
val USER_NAME = stringPreferencesKey("user_name")
val USER_AGE = intPreferencesKey("user_age")
val DARK_MODE = booleanPreferencesKey("dark_mode")
}
注意:preferencesDataStore 委托只能用于顶级声明,且一个文件只能创建一个 DataStore 实例,避免创建多个同名存储文件。
读取数据
读取操作是异步的,返回 Flow,不会阻塞主线程。在协程中收集即可:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class UserSettingsRepository(private val context: Context) {
// 读取字符串
val userNameFlow: Flow<String> = context.dataStore.data
.map { preferences ->
preferences[PreferenceKeys.USER_NAME] ?: "未设置"
}
// 读取布尔值
val darkModeFlow: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[PreferenceKeys.DARK_MODE] ?: false
}
}
在 UI 中使用 collectAsState 或 launchWhenStarted 即可实时响应变化。
写入数据
写入需使用 edit() 挂起函数,内部自动提供事务保护:
import kotlinx.coroutines.flow.first
suspend fun updateUserName(name: String) {
context.dataStore.edit { preferences ->
preferences[PreferenceKeys.USER_NAME] = name
}
}
suspend fun toggleDarkMode() {
context.dataStore.edit { preferences ->
val current = preferences[PreferenceKeys.DARK_MODE] ?: false
preferences[PreferenceKeys.DARK_MODE] = !current
}
}
edit 中的 lambda 运行在 IO 线程,无需手动切换线程。写入是原子的,所有操作完成后才会持久化。
观察数据变化
由于读取返回 Flow,可以直接配合 LiveData 或 Compose 状态:
Compose 示例:
@Composable
fun UserProfileScreen(repository: UserSettingsRepository) {
val userName by repository.userNameFlow.collectAsState(initial = "加载中...")
val darkMode by repository.darkModeFlow.collectAsState(initial = false)
Column {
Text("用户名:$userName")
Switch(
checked = darkMode,
onCheckedChange = {
// 在协程中更新
scope.launch { repository.toggleDarkMode() }
}
)
}
}
错误处理与异常
DataStore 的读写可能抛出 IOException 或 CorruptionException。读取时可以捕获异常,提供默认值:
val safeFlow: Flow<String> = context.dataStore.data
.catch { exception ->
// 处理异常
if (exception is IOException) {
emit(emptyPreferences()) // 发出空值,继续运行
} else {
throw exception
}
}
.map { preferences ->
preferences[PreferenceKeys.USER_NAME] ?: "默认名"
}
协程写操作也建议用 try-catch 包裹,保证程序稳定。
从 SharedPreferences 迁移
如果你的应用已经使用 SharedPreferences,DataStore 提供了内置迁移方法:
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "old_prefs_name"))
}
)
迁移会一次性将 SharedPreferences 中的数据导入 DataStore,完成后原有的 SharedPreferences 文件会被清空。注意:迁移只会在 DataStore 第一次创建时执行。
Proto DataStore 简要入门
当需要存储结构化对象,且对类型安全要求极高时,Proto DataStore 是更优选择。
步骤概览
- 定义 .proto 文件(位于
app/src/main/proto/):
syntax = "proto3";
option java_package = "com.example.app.proto";
option java_multiple_files = true;
message UserSettings {
string user_name = 1;
int32 user_age = 2;
bool dark_mode = 3;
}
- 配置 protobuf 插件(在
build.gradle.kts):
plugins {
id("com.google.protobuf") version "0.9.4"
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.24.4" }
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") { option("lite") }
}
}
}
}
- 创建 Serializer 与 DataStore:
import androidx.datastore.core.Serializer
import java.io.InputStream
import java.io.OutputStream
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings =
try {
UserSettings.parseFrom(input)
} catch (e: Exception) {
throw CorruptionException("Cannot read proto.", e)
}
override suspend fun writeTo(t: UserSettings, output: OutputStream) =
t.writeTo(output)
}
val Context.protoDataStore by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
- 读写操作:
suspend fun updateProtoSettings(current: UserSettings) {
context.protoDataStore.updateData { settings ->
settings.toBuilder()
.setUserName("新名字")
.setDarkMode(true)
.build()
}
}
读取同样返回 Flow<UserSettings>,用法与 Preferences DataStore 类似。
最佳实践与注意事项
- 避免在主线程阻塞:所有写入都是挂起函数,必须在协程中调用。
- 不要创建多个同名 DataStore 实例:单例或依赖注入避免重复实例化,否则可能导致文件损坏。
- 键名一致性:Preferences DataStore 的键最好集中在 object 中,防止字符串硬编码。
- 控制存储文件大小:DataStore 设计用于轻量级数据,避免存入超大数据集(如序列化 Bitmap)。大文件请使用 Room 或文件存储。
- 清理敏感数据:退出登录等场景可通过
edit清除所有键,或删除文件重新创建。 - 测试友好:DataStore 的路径可自定义,测试时可传入
TestScope下的临时文件夹,方便单元测试。
总结
DataStore 以协程和 Flow 为基础,为 Android 本地存储提供了现代、安全、高效的解决方案。Preferences DataStore 上手成本低,无缝替代 SharedPreferences;Proto DataStore 则进一步保障类型安全,适合复杂模型。立即在你的新项目中替换 SharedPreferences,享受异步无阻塞的数据存储体验吧!