DataStore 简介
- 替代 SharedPreferences 的数据存储方案。
- 分为 Preferences(键值对)与 Proto(类型安全)。
- 支持 Kotlin Flow,天然异步。
Preferences DataStore
val Context.settingsDataStore by preferencesDataStore(name = "settings")
object SettingsKeys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
}
suspend fun setDarkMode(enabled: Boolean, context: Context) {
context.settingsDataStore.edit { prefs ->
prefs[SettingsKeys.DARK_MODE] = enabled
}
}
val darkModeFlow: Flow<Boolean> = context.settingsDataStore.data
.map { prefs -> prefs[SettingsKeys.DARK_MODE] ?: false }
Proto DataStore
1. 定义 proto
user_prefs.proto
syntax = "proto3";
option java_package = "com.example.datastore";
message UserPreferences {
string userId = 1;
string locale = 2;
}
2. 配置 Gradle
plugins {
id("com.google.protobuf") version "0.9.4"
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.25.3" }
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java")
create("kotlin")
}
}
}
}
3. 使用 ProtoDataStore
val Context.userPreferencesDataStore: DataStore<UserPreferences> by dataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer
)
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences =
try {
UserPreferences.parseFrom(input)
} catch (e: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", e)
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
SharedPreferences 迁移
val Context.migratedDataStore by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer,
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "old_settings"))
}
)
迁移逻辑可在 SharedPreferencesMigration.migrate 中自定义。
多模块集成
- 在
core/datastore模块封装公共接口。 - 通过 Hilt
@Provides注入DataStore实例。
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
@Provides
fun provideUserPreferences(@ApplicationContext context: Context): DataStore<UserPreferences> {
return context.userPreferencesDataStore
}
}
测试与调试
@RunWith(AndroidJUnit4::class)
class DataStoreTest {
@get:Rule
val tmpFolder = TemporaryFolder()
private val testContext = ApplicationProvider.getApplicationContext<Context>()
.createDeviceProtectedStorageContext()
@Test
fun writeAndRead() = runTest {
val store = PreferenceDataStoreFactory.create(
scope = TestScope(UnconfinedTestDispatcher()),
produceFile = { tmpFolder.newFile("settings.preferences_pb") }
)
store.edit { it[booleanPreferencesKey("flag")] = true }
assertTrue(store.data.first()[booleanPreferencesKey("flag")] == true)
}
}
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 数据读取卡住 | 未消费 Flow | 使用 collect 或 first 获取数据 |
| Proto 解析失败 | 序列化字段缺失 | 升级 schema 并提供默认值 |
| UI 不更新 | 未切换到主线程 | flowOn(Dispatchers.IO) + collect 在 Main |
实战案例:SharedPreferences 迁移是”开弓没有回头箭”
Google 官方博客《Prefer Storing Data with Jetpack DataStore》里,迁移 API 长这样:
val dataStore: DataStore<Preferences> = context.createDataStore(
name = "settings",
migrations = listOf(SharedPreferencesMigration(context, "settings_preferences"))
)
看起来很美好,但有三个实战细节文档不会大声告诉你:
- 迁移只跑一次。
SharedPreferencesMigration在 DataStore 第一次被访问时执行,成功后就把旧 XML 文件删了。这意味着:迁移代码有 bug,线上用户的数据就没了,没有第二次机会。必须先在测试环境用真实的老版本数据跑一遍。 - 键名必须对上。DataStore 的
stringPreferencesKey("username")和 SharedPreferences 的"username"是同一个字符串,改名即丢失。重构时顺手”优化”键名是迁移失败的头号原因。 - 别在迁移里做耗时操作。迁移跑在 DataStore 初始化路径上,卡住就等于 App 启动卡住。
我在 Flutter 项目里(某物流冷链项目)token 还存在 SharedPreferences 明文里——按 Android 的标准答案,早该迁到 DataStore(加密版走 Tink)。知道和做到之间差的是排期,但方向是明确的:新项目直接上 DataStore,别再写 SharedPreferences。
参考:Android Developers Blog《Prefer Storing Data with Jetpack DataStore》
总结
- DataStore 更适合现代 Kotlin 异步流程,与 Flow、协程配合紧密。
- Preferences 模式简单易用,Proto 模式提供类型安全与扩展性。
- 合理设计迁移方案,可平滑替代旧的 SharedPreferences,保障数据连续性。
