一、 为何要告别 SharedPreferences?
在 Android 开发中,如果你曾经用过 SharedPreferences 来保存用户的设置或者简单的数据,那你一定对它又爱又恨。爱的是它用起来真的非常简单,恨的是它在处理复杂数据、类型安全以及异步操作时,常常让人头疼。比如,你想存一个用户对象,里面有名字、年龄和邮箱,用 SharedPreferences 就得一个个字段拆开存,取的时候再拼起来,不仅麻烦,还容易出错。更重要的是,它在主线程上进行磁盘 I/O 操作,虽然提供了 apply() 来异步写入,但依然可能在某些场景下引发性能问题或 ANR(应用无响应)。
随着应用越来越复杂,我们对数据存储的要求也更高了:需要类型安全、支持异步、易于维护,并且能处理更结构化的数据。这就是 Google 推出 Jetpack DataStore 的初衷。DataStore 提供了两种解决方案:Preferences DataStore 和 Proto DataStore。前者类似于 SharedPreferences 的升级版,用键值对存储;后者则更强大,它使用 Protocol Buffers 来定义数据结构,提供了强类型保证和更高的灵活性。今天,我们就重点聊聊如何从熟悉的 SharedPreferences 迁移到更强大的 Proto DataStore,实现一次平滑的升级。
二、 认识新朋友:Proto DataStore 的核心概念
在开始动手迁移之前,我们得先了解 Proto DataStore 的几个关键点,这能帮你更好地理解后续的步骤。
首先,Protocol Buffers(简称 Protobuf) 是 Google 的一种数据序列化机制。你可以把它想象成一种更高效、更强大的“数据契约”,用它来定义你的数据结构。在 Android 里,我们需要通过一个 .proto 文件来定义这个契约。
其次,DataStore 是异步和基于流的。所有读写操作都是通过 Kotlin 协程或者 RxJava 的 Flow 来完成的,这意味着它默认就是非阻塞的,非常适合现代 Android 应用的开发模式。
最后,迁移是关键一步。DataStore 提供了专门的 API,可以将旧的 SharedPreferences 数据完整地导入到新的 DataStore 中,确保用户升级应用时不丢失任何设置。
听起来可能有点抽象,别担心,接下来我们会用具体的例子,一步一步带你走完整个流程。
三、 迁移实战:从定义数据契约开始
迁移的第一步,是为你的数据定义一个 Protobuf 契约。假设我们原来用 SharedPreferences 存了用户的主题模式(深色/浅色)和消息通知开关。现在,我们要用 Proto DataStore 来管理这些设置。
技术栈:Kotlin + Android Jetpack
1. 设置 Protobuf 依赖
在你的模块级 build.gradle.kts 文件中添加必要的插件和依赖:
// 注意:这是Kotlin DSL的写法
plugins {
id("com.android.application")
kotlin("android")
// 添加protobuf插件
id("com.google.protobuf") version "0.9.4"
}
android {
// ... 你的其他配置
}
dependencies {
// DataStore 依赖
implementation("androidx.datastore:datastore:1.0.0")
implementation("androidx.datastore:datastore-preferences:1.0.0")
// Proto DataStore 需要protobuf运行时
implementation("com.google.protobuf:protobuf-javalite:3.21.12")
}
// 配置protobuf插件
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.21.12"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
}
2. 创建 .proto 文件定义数据结构
在 app/src/main/proto/ 目录下(如果没有就新建),创建一个名为 user_preferences.proto 的文件。
// 指定使用的protobuf语法版本
syntax = "proto3";
// 选项,生成轻量级的Java类,更适合Android
option java_package = "com.example.myapp.datastore";
option java_multiple_files = true;
// 定义消息类型,这对应我们想要存储的数据结构
message UserPreferences {
// 字段定义:类型 字段名 = 唯一的数字标签;
// 数字标签一旦定义,后续就不能再更改,这是protobuf的兼容性要求
string theme_mode = 1; // 主题模式,如 "light", "dark"
bool notifications_enabled = 2; // 通知开关
int32 login_count = 3; // 新增字段:登录次数,展示扩展性
}
编译项目后,Protobuf插件会自动为我们生成 UserPreferences 这个Java类,它包含了所有字段的getter/setter,以及构建器(Builder)模式的方法。
四、 构建 DataStore 并实现迁移
定义好数据结构后,我们需要创建 DataStore 实例,并告诉它如何从旧的 SharedPreferences 读取数据。
1. 创建 Serializer
Serializer 告诉 DataStore 如何将我们定义的 Protobuf 消息类型和磁盘上的字节进行转换。
package com.example.myapp.datastore
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.example.myapp.datastore.UserPreferences // 自动生成的类
import java.io.InputStream
import java.io.OutputStream
// 1. 实现 Serializer 接口
object UserPreferencesSerializer : Serializer<UserPreferences> {
// 默认值,如果文件不存在或为空,则返回这个值
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
// 使用 Protobuf 生成类的 parseFrom 方法从流中读取
return UserPreferences.parseFrom(input)
} catch (exception: Exception) {
// 如果文件损坏或格式不对,抛异常并返回默认值
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
// 使用 Protobuf 生成类的 writeTo 方法写入流
t.writeTo(output)
}
}
// 2. 通过扩展属性创建 DataStore 实例
// 这会在 Context 上创建一个名为“user_preferences.pb”的文件
val Context.userDataStore: DataStore<UserPreferences> by dataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer
)
2. 实现迁移逻辑
这是最关键的一步。我们需要定义一个从 SharedPreferences 到 UserPreferences 的映射关系。
package com.example.myapp.datastore
import android.content.Context
import androidx.datastore.core.DataMigration
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
// 旧的 SharedPreferences 名称(假设你原来用的这个名字)
private const val OLD_PREFS_NAME = "my_old_settings"
// 1. 为旧的 Preferences DataStore 定义扩展(用于读取旧数据)
val Context.oldPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(
name = OLD_PREFS_NAME
)
// 2. 创建迁移对象
val sharedPrefsMigration = object : DataMigration<UserPreferences> {
override suspend fun shouldMigrate(currentData: UserPreferences): Boolean {
// 这里我们可以添加一些逻辑来判断是否需要迁移
// 例如,检查旧文件是否存在。但通常 DataStore 会处理。
// 简单返回 true 表示总是尝试迁移。
return true
}
override suspend fun migrate(currentData: UserPreferences): UserPreferences {
// 从旧的 Preferences DataStore 中读取数据
val oldPrefs = context.oldPreferencesDataStore.data.first()
// 构建新的 UserPreferences 对象
return UserPreferences.newBuilder(currentData).apply {
// 映射旧键名到新字段
// 注意:这里假设旧键名是 "theme_mode" 和 "notifications_enabled"
oldPrefs[stringPreferencesKey("theme_mode")]?.let { theme ->
themeMode = theme // 直接赋值,类型是 String
}
oldPrefs[booleanPreferencesKey("notifications_enabled")]?.let { enabled ->
notificationsEnabled = enabled // 直接赋值,类型是 Boolean
}
// 旧数据中没有 login_count,所以使用默认值(0)
}.build()
}
override suspend fun cleanUp() {
// 迁移完成后,可以在这里进行清理工作,比如删除旧文件。
// 但务必谨慎,确保数据已成功迁移并验证。
// context.getSharedPreferences(OLD_PREFS_NAME, Context.MODE_PRIVATE).edit().clear().apply()
println("迁移完成,可在此清理旧资源。")
}
}
// 3. 更新 DataStore 创建,加入迁移
val Context.userDataStoreWithMigration: DataStore<UserPreferences> by dataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer,
// 传入迁移列表,可以传入多个迁移,按顺序执行
migrations = listOf(sharedPrefsMigration)
)
现在,当应用首次使用 userDataStoreWithMigration 时,DataStore 会自动执行 shouldMigrate 和 migrate 方法,将旧数据搬到新家。
五、 使用新 DataStore 读写数据
迁移完成后,我们就可以愉快地使用新的 DataStore 了。它的 API 非常简洁,并且基于 Flow。
package com.example.myapp.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapp.datastore.userDataStoreWithMigration
import com.example.myapp.datastore.UserPreferences
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class SettingsViewModel(private val context: Context) : ViewModel() {
// 1. 读取数据:将 DataStore 的 Flow 映射成 UI 需要的状态
val userPreferencesFlow = context.userDataStoreWithMigration.data
.map { userPrefs ->
// 这里可以进行数据转换
SettingsUiState(
themeMode = userPrefs.themeMode,
notificationsEnabled = userPrefs.notificationsEnabled,
loginCount = userPrefs.loginCount
)
}
// 2. 更新数据
fun updateThemeMode(newTheme: String) {
viewModelScope.launch {
context.userDataStoreWithMigration.updateData { currentPrefs ->
// updateData 提供了一个原子性的“读-改-写”操作
currentPrefs.toBuilder()
.setThemeMode(newTheme)
.build()
}
}
}
fun incrementLoginCount() {
viewModelScope.launch {
context.userDataStoreWithMigration.updateData { currentPrefs ->
currentPrefs.toBuilder()
.setLoginCount(currentPrefs.loginCount + 1)
.build()
}
}
}
}
// 一个简单的UI状态数据类
data class SettingsUiState(
val themeMode: String = "light",
val notificationsEnabled: Boolean = true,
val loginCount: Int = 0
)
在 Activity 或 Fragment 中,你可以像观察其他 LiveData 或 Flow 一样观察 userPreferencesFlow,并调用 ViewModel 的方法来更新数据。整个过程都是类型安全的,themeMode 一定是 String,notificationsEnabled 一定是 Boolean,编译器会帮你检查,大大减少了运行时错误。
六、 深入分析:场景、优缺点与注意事项
应用场景
Proto DataStore 非常适合管理结构化、需要类型安全且可能随时间演变的配置数据。例如:用户个人资料、应用设置、功能开关、本地缓存的数据模型等。如果你的数据只是几个简单的、无关的开关,Preferences DataStore 可能更轻量。但如果你的数据有关联性,或者未来可能会增加字段,Proto DataStore 的扩展性优势就非常明显。
技术优缺点
优点:
- 强类型安全:在编译期就能发现类型错误,避免
SharedPreferences中getString错拿成getInt这类问题。 - 支持复杂数据结构:通过 Protobuf 可以轻松定义嵌套对象、列表等。
- 异步与线程安全:基于 Flow 的 API 天然支持异步,读写操作不会阻塞主线程。
- 数据一致性:
updateData提供了原子性操作,避免了并发读写冲突。 - 良好的扩展性:Protobuf 协议向后兼容性很好,可以安全地添加新字段,旧版本应用读取新数据也不会崩溃。
缺点:
- 学习曲线:需要理解 Protobuf 的基本概念和语法,增加了初始复杂度。
- 配置稍繁琐:需要配置 Gradle 插件和编写
.proto文件,对于简单需求显得“杀鸡用牛刀”。 - 不能直接查看内容:生成的是二进制文件,不像
SharedPreferences的 XML 文件可以直接在设备文件浏览器中查看和调试。
注意事项
- 一次迁移:DataStore 的迁移通常只发生一次(当旧数据文件存在且新文件第一次被创建时)。确保你的迁移逻辑正确且完整。
- 默认值:在 Serializer 中定义的
defaultValue很重要,它是数据文件的“基石”。 - 字段标签不可更改:
.proto文件中的字段数字标签(如= 1)一旦被使用,就永远不能更改或重复使用,这是 Protobuf 保持前后兼容的核心规则。新增字段请使用新的、从未用过的数字标签。 - 逐步迁移:对于大型应用,不必一次性迁移所有
SharedPreferences。可以按功能模块逐个迁移,降低风险。 - 测试:务必为迁移逻辑编写单元测试和集成测试,模拟各种旧数据情况,确保迁移万无一失。
七、 总结
从 SharedPreferences 迁移到 Proto DataStore,看似步骤不少,但实际上是一条通往更健壮、更可维护数据存储层的必经之路。它用前期稍微多一点的配置和设计,换来了长远的开发效率提升和运行时稳定性。
整个过程可以概括为:定义契约(.proto) -> 实现序列化(Serializer) -> 规划迁移(DataMigration) -> 安全切换(更新DataStore实例) -> 享受新特性(类型安全、异步Flow)。
希望这篇指南能像一张清晰的地图,帮你顺利完成这次重要的“数据基础设施”升级。记住,每一次技术的升级,都是为了给用户带来更稳定流畅的体验,同时也是开发者自身技能树的一次宝贵拓展。现在,就打开你的项目,开始尝试吧!
评论