一、老伙计SharedPreferences,我们该说再见了
在Android开发的漫长岁月里,SharedPreferences 就像一位忠实的老伙计,帮我们存储一些简单的配置信息,比如用户是否打开了夜间模式、上次登录的用户名等等。它的用法简单直接,几行代码就能搞定,因此深受一代又一代开发者的喜爱。
但是,随着应用越来越复杂,我们对数据存储的要求也越来越高。这位“老伙计”开始暴露出一些力不从心的地方:
- 它不是线程安全的:在主线程读写还好,一旦在多个线程里同时操作,就很容易出现数据错乱。
- 同步API可能导致界面卡顿:它的
commit()是同步的,apply()虽然是异步,但在某些旧版本系统上依然可能引发ANR(应用无响应)。 - 没有错误处理机制:如果存储过程出错,它不会告诉你,数据可能就悄无声息地丢失了。
- 不支持复杂数据类型:只能存基本类型和
String,想存个对象列表?得自己序列化成String,很麻烦。
Google也看到了这些问题,于是在Jetpack组件库中,为我们带来了两位新朋友:Preferences DataStore 和 Proto DataStore。今天,我们重点聊聊更贴近SharedPreferences使用习惯的 Preferences DataStore。它旨在解决上述所有痛点,提供一种更安全、更现代、基于协程的异步数据存储方案。
二、初识Preferences DataStore:它是什么,怎么用?
简单来说,Preferences DataStore 就是一个升级版的、更聪明的“键值对”存储工具。它底层依然使用文件存储,但通过Kotlin协程和Flow,让所有操作都变得异步化、可观察,并且保证了线程安全。
技术栈声明:本文所有示例均基于 Kotlin + Android Jetpack 技术栈。
让我们从一个完整的示例开始,看看如何用它来存储和读取一个简单的用户设置。
首先,在项目的 build.gradle 文件中添加依赖:
// 在模块级的 build.gradle.kts 中
dependencies {
implementation("androidx.datastore:datastore-preferences:1.1.1")
// 如果你需要RxJava支持,可以添加
// implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")
// implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
}
接下来,我们创建一个数据存储的管理类。这是推荐的做法,可以集中管理所有的数据存储操作。
// 技术栈:Kotlin + Android Jetpack
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey // 用于布尔值
import androidx.datastore.preferences.core.edit // 用于编辑数据
import androidx.datastore.preferences.core.intPreferencesKey // 用于整型
import androidx.datastore.preferences.core.stringPreferencesKey // 用于字符串
import androidx.datastore.preferences.preferencesDataStore // 属性委托扩展
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
// 1. 在Context上创建一个扩展属性,方便全局访问DataStore实例。
// 这里的`user_preferences`是存储文件的名称,类似于SharedPreferences的文件名。
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_preferences")
class UserPreferencesManager(private val context: Context) {
// 2. 定义键(Key)。这相当于SharedPreferences里的键名,但它是类型安全的。
companion object {
val IS_DARK_MODE = booleanPreferencesKey("is_dark_mode")
val USER_NAME = stringPreferencesKey("user_name")
val NOTIFICATION_COUNT = intPreferencesKey("notification_count")
}
// 3. 读取数据:返回一个Flow,可以持续观察数据变化。
val isDarkMode: Flow<Boolean> = context.dataStore.data
.map { preferences ->
// 如果不存在,则提供默认值false
preferences[IS_DARK_MODE] ?: false
}
val userName: Flow<String> = context.dataStore.data
.map { preferences ->
preferences[USER_NAME] ?: ""
}
// 4. 写入数据:挂起函数,必须在协程或另一个挂起函数中调用。
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[IS_DARK_MODE] = enabled
}
}
suspend fun setUserName(name: String) {
context.dataStore.edit { preferences ->
preferences[USER_NAME] = name
}
}
// 一个更复杂的例子:递增计数
suspend fun incrementNotificationCount() {
context.dataStore.edit { preferences ->
val currentCount = preferences[NOTIFICATION_COUNT] ?: 0
preferences[NOTIFICATION_COUNT] = currentCount + 1
}
}
}
在上面的代码中,有几个关键点:
preferencesDataStore:这是一个属性委托,确保在应用生命周期内,同一个文件名的DataStore只被创建一次,这是单例的。*PreferencesKey:类型安全的键。你用什么类型的Key,就决定了存储和读取的数据类型,编译器会帮你检查,避免了类型转换错误。Flow:这是DataStore读取数据的核心。Flow是Kotlin协程库中的“响应式流”,你可以把它想象成一个可以不断发射数据的水管。当存储的数据发生变化时,所有收集这个Flow的地方都会自动收到最新的值,这非常适合更新UI。edit:用于写入数据的挂起函数。它提供了一个MutablePreferences的上下文,让你可以安全地修改数据。
三、在Activity或ViewModel中使用它
创建好管理类后,我们就可以在界面层使用了。通常,我们会在ViewModel中持有UserPreferencesManager的实例,然后在Activity或Fragment中观察Flow。
// 技术栈:Kotlin + Android Jetpack
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class SettingsViewModel(private val userPrefsManager: UserPreferencesManager) : ViewModel() {
// 将Flow直接暴露给UI层观察
val isDarkModeFlow = userPrefsManager.isDarkMode
val userNameFlow = userPrefsManager.userName
// 更新用户名的函数
fun updateUserName(newName: String) {
viewModelScope.launch {
// 在ViewModel的协程作用域内调用挂起函数
userPrefsManager.setUserName(newName)
}
}
// 切换夜间模式的函数
fun toggleDarkMode() {
viewModelScope.launch {
// 先读取当前状态,然后取反写入
// 注意:这里为了演示,直接通过Flow的`first()`挂起函数获取当前值。
// 在实际项目中,可能需要在Manager中提供一个同步或一次性读取的方法。
val currentMode = userPrefsManager.isDarkMode.first()
userPrefsManager.setDarkMode(!currentMode)
}
}
}
在Activity或Fragment中,我们观察ViewModel暴露的Flow:
// 技术栈:Kotlin + Android Jetpack
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class SettingsActivity : AppCompatActivity() {
private val viewModel: SettingsViewModel by viewModels {
// 假设通过依赖注入或工厂模式提供ViewModel
ViewModelFactory(application)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
// 观察夜间模式状态,并更新UI(例如Switch按钮)
lifecycleScope.launch {
// repeatOnLifecycle 是更安全的方式,确保只在界面活跃时收集流,避免资源浪费。
// 这里为了简化,使用launch。实际建议使用`repeatOnLifecycle(Lifecycle.State.STARTED)`
viewModel.isDarkModeFlow.collect { isDarkMode ->
switchDarkMode.isChecked = isDarkMode
// 可以根据isDarkMode应用主题
}
}
// 观察用户名
lifecycleScope.launch {
viewModel.userNameFlow.collect { userName ->
textViewUserName.text = "欢迎,$userName"
}
}
// 按钮点击事件,触发更新
buttonSave.setOnClickListener {
val newName = editTextName.text.toString()
viewModel.updateUserName(newName)
Toast.makeText(this, "用户名已更新", Toast.LENGTH_SHORT).show()
}
switchDarkMode.setOnCheckedChangeListener { _, isChecked ->
// 注意:这里直接调用toggle,但更好的做法是响应switch变化,调用viewModel.setDarkMode(isChecked)
viewModel.toggleDarkMode()
}
}
}
通过这个例子,你可以看到整个数据流非常清晰:UI触发事件 -> ViewModel调用挂起函数写入DataStore -> DataStore数据变更 -> Flow发射新数据 -> UI层自动收到更新并刷新界面。这是一个完整的、响应式的数据循环。
四、迁移与共存:如何从SharedPreferences平滑过渡?
如果你已经有一个使用SharedPreferences的旧项目,别担心,DataStore提供了非常方便的迁移工具,可以让你把旧数据自动迁移到新系统中。
// 技术栈:Kotlin + Android Jetpack
import androidx.datastore.core.DataMigration
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
// 1. 定义一个迁移器
val sharedPrefsMigration = object : DataMigration<Preferences> {
override suspend fun shouldMigrate(currentData: Preferences): Boolean {
// 这里可以添加逻辑判断是否需要迁移。
// 例如,检查旧SharedPreferences文件是否存在且有数据。
// 为了简单,我们假设总是需要迁移。
return true
}
override suspend fun migrate(currentData: Preferences): Preferences {
// 在这里执行具体的迁移逻辑
val sharedPrefs = context.getSharedPreferences("old_shared_prefs_name", Context.MODE_PRIVATE)
val mutablePreferences = currentData.toMutablePreferences()
// 迁移布尔值
sharedPrefs.getBoolean("old_dark_mode_key", false)?.let {
mutablePreferences[UserPreferencesManager.IS_DARK_MODE] = it
}
// 迁移字符串
sharedPrefs.getString("old_user_name_key", "")?.let {
mutablePreferences[UserPreferencesManager.USER_NAME] = it
}
// ... 迁移其他所有需要的键值对
return mutablePreferences.toPreferences()
}
override suspend fun cleanUp() {
// 迁移完成后,可以在这里清理旧的SharedPreferences文件。
// context.deleteSharedPreferences("old_shared_prefs_name")
}
}
// 2. 在创建DataStore时应用这个迁移器
private val Context.dataStoreWithMigration: DataStore<Preferences> by preferencesDataStore(
name = "user_preferences",
produceMigrations = { context ->
// 可以添加多个迁移器,按顺序执行
listOf(sharedPrefsMigration)
}
)
将UserPreferencesManager中的context.dataStore替换为context.dataStoreWithMigration。当应用第一次运行时,DataStore会自动执行shouldMigrate和migrate方法,将旧数据搬运过来,之后就会使用新的DataStore文件。cleanUp方法给了你一个安全删除旧文件的机会。
五、深入分析:场景、优缺点与注意事项
应用场景:
- 用户设置与偏好:这是最经典的场景,如主题、语言、音效开关、字体大小等。
- 应用配置:首次启动标志、API环境配置、功能开关等。
- 简单的用户状态:登录令牌(建议加密)、用户ID、上次访问时间等。
- 需要响应式更新的数据:任何一处修改,需要即时在UI多处反映的数据,用
Flow非常合适。
技术优点:
- 线程安全:底层实现保证了并发读写安全,开发者无需自己加锁。
- 异步与响应式:基于协程和
Flow,主线程永不阻塞,且能轻松实现数据变化的监听。 - 类型安全:通过预定义的
Key,在编译期就能发现类型错误。 - 支持错误处理:
dataStore.data这个Flow在发生IO错误时会抛出异常,你可以用catch操作符进行处理。 - 更好的迁移支持:提供了清晰的API来处理从
SharedPreferences或其他来源的数据迁移。
技术缺点与注意事项:
- 学习成本:需要理解Kotlin协程和
Flow的基本概念,对于不熟悉响应式编程的开发者有一定门槛。 - API复杂度:相比
SharedPreferences的getBoolean()和edit().putBoolean().apply(),DataStore的初始化、键定义、读写分离的步骤稍多。 - 部分操作需要协程:所有写操作和复杂读操作都必须是挂起函数,意味着你必须在协程作用域内调用它们。
- 不能用于多进程:和
SharedPreferences一样,Preferences DataStore不支持多进程应用。如果应用需要多进程共享数据,需要考虑其他方案,如Room数据库。 - 数据大小限制:虽然它没有硬性限制,但和
SharedPreferences一样,它本质上不适合存储大量数据或复杂结构。对于列表或对象,应考虑Proto DataStore(使用Protocol Buffers)或直接使用Room。
六、文章总结
总的来说,Android Jetpack Preferences DataStore 是Google官方推荐的、用于替代SharedPreferences的现代化解决方案。它并非要完全颠覆旧有概念,而是在其熟悉的“键值对”模型上,注入了协程、Flow、类型安全、线程安全等现代编程的优势。
对于新项目,强烈建议直接从DataStore开始。对于老项目,可以利用其完善的迁移机制逐步替换,享受新技术带来的稳定性和开发效率提升。虽然初期需要适应协程和Flow的思维,但一旦掌握,你会发现它让异步数据处理的代码变得异常简洁和优雅。
告别SharedPreferences在主线程上的潜在卡顿,告别手动处理线程同步的烦恼,拥抱DataStore,让你的应用数据层更加健壮和可维护。这不仅是技术的升级,更是开发理念向响应式、异步化的一次迈进。
评论