Android DataStore 替代 SharedPreferences

FreeGuideOnline 最新 2026-07-12

groovy dependencies { // Preferences DataStore (仅键值对) implementation "androidx.datastore:datastore-preferences:1.1.1"

// Proto DataStore (结构化数据)
implementation "androidx.datastore:datastore-core:1.1.1"
implementation "com.google.protobuf:protobuf-javalite:3.25.3"

// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"

}


> 注意:如果同时使用 Proto DataStore,需要在项目根 `build.gradle` 添加 Protobuf 插件。

## 快速上手 Preferences DataStore
它提供了与 SharedPreferences 类似的键值对模式,但 API 完全基于 Flow。

### 1. 创建 DataStore 实例
使用属性委托 `preferencesDataStore` 避免多次创建同一实例:

```kotlin
val Context.dataStore by preferencesDataStore(name = "settings")

推荐将实例置于顶层扩展属性,如 SettingsRepository 中。

2. 定义键 (Key)

使用 preferencesKey<T>() 指定键名和明确类型:

object PreferencesKeys {
    val USER_NAME = stringPreferencesKey("user_name")
    val AGE = intPreferencesKey("age")
    val SHOW_NOTIFICATIONS = booleanPreferencesKey("show_notifications")
}

3. 写入数据

所有写操作都是挂起函数,通过 edit 完成,在事务中原子提交:

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

4. 读取数据

返回 Flow<T?>,可观察变化,读取不会阻塞线程:

val userNameFlow: Flow<String> = context.dataStore.data
    .map { preferences ->
        preferences[PreferencesKeys.USER_NAME] ?: "未设置"
    }

在 Activity/Fragment 中收集:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        userNameFlow.collect { name ->
            textView.text = name
        }
    }
}

5. 捕获异常

DataStore 抛出 IOException 当底层文件访问失败,可使用 catch 操作符优雅处理:

val safeFlow: Flow<String> = context.dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences()) // 发送空值,让下游使用默认值
        } else {
            throw exception
        }
    }
    .map { preferences ->
        preferences[PreferencesKeys.USER_NAME] ?: "未知"
    }

Proto DataStore:结构化首选

当需要存储复杂对象(如用户配置、应用状态)时,使用 Protocol Buffers 获得编译时类型安全。

1. 定义 .proto 文件

app/src/main/proto/ 下创建 user_prefs.proto

syntax = "proto3";

option java_package = "com.example.app.datastore";
option java_multiple_files = true;

message UserPreferences {
  string name = 1;
  int32 age = 2;
  repeated string interests = 3;   // 列表
  bool has_pro_account = 4;
}

构建项目,编译器将自动生成 Java/Kotlin 数据类。

2. 实现序列化器

将 proto 对象与磁盘格式相互转换:

object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    
    override suspend fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }
    
    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        t.writeTo(output)
    }
}

3. 创建 Proto DataStore

val Context.userPreferencesDataStore by dataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer
)

4. 读写操作

写入通过 updateData,它提供了当前对象以便部分修改:

suspend fun updateUserAge(newAge: Int) {
    context.userPreferencesDataStore.updateData { currentPreferences ->
        currentPreferences.toBuilder()
            .setAge(newAge)
            .build()
    }
}

读取仍然是 Flow

val userPreferencesFlow: Flow<UserPreferences> = context.userPreferencesDataStore.data

从 SharedPreferences 平稳迁移

DataStore 提供了一次性导入机制,将旧数据自动转移至新存储。

1. 创建带迁移的 DataStore

通过 SharedPreferencesMigration 指定旧 SP 文件名:

val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, "legacy_prefs"))
    }
)

构建 DataStore 时,如果旧 SP 存在且未迁移,它的所有键值对会被复制到 DataStore,之后旧文件被删除。

2. 手动处理键映射(可选)

如果新旧键名不同或需要类型转换,可提供映射函数:

SharedPreferencesMigration(
    context,
    "legacy_prefs",
    shouldRunMigration = { true },    // 可根据条件判断是否迁移
    keyMapper = { oldKey, _ ->
        // 将旧键映射为新键
        when (oldKey) {
            "old_name_key" -> USER_NAME.name
            else -> null   // 忽略该键
        }
    }
)