26、DataStore:告别SharedPreferences,拥抱现代化存储

说实话,做Android开发这么多年,SharedPreferences 就像一位老朋友。它简单、直接,几乎每个项目都用过。但这位老朋友,其实有不少“坏毛病”。

我记得刚入行那会儿,在一个日活百万的App里,就因为SP的同步写入,导致UI线程卡顿,被用户骂惨了。后来排查才发现,是某个第三方SDK在频繁写SP。

所以,当Jetpack DataStore出现时,我几乎是第一时间就切了过去。今天,我们就来彻底聊聊它。

DataStore vs SharedPreferences:为什么换?

先看一张对比表,你就明白了。

对比维度 SharedPreferences DataStore
线程安全 不保证(容易ANR) 基于Flow,异步安全
数据一致性 写入时可能丢失 事务性写入,原子操作
异常处理 运行时异常 返回Result,可捕获
类型安全 仅支持基本类型+String 支持自定义对象(Proto)
监听变化 OnSharedPreferenceChangeListener(易泄漏) Flow,响应式,生命周期安全

说白了,SP是Java早期设计的产物,而DataStore是Kotlin协程时代的亲儿子。如果你还在用SP,我建议你认真考虑迁移。

⚠️ 避坑指南: 我曾经在迁移一个老项目时,发现SP里存了JSON字符串。直接用DataStore的String类型存没问题,但要注意编码。另外,SP的apply()是异步的,但commit()是同步的,很多人混用导致bug。

Preferences DataStore:键值对存储的“轻骑兵”

如果你只是存一些简单的配置,比如用户是否登录、主题颜色、音量大小,那用 Preferences DataStore 就够了。它和SP的用法很像,但底层完全重写了。

添加依赖

// build.gradle (app)
implementation "androidx.datastore:datastore-preferences:1.0.0"

创建DataStore实例

我个人习惯用顶层属性来持有单例,这样全局都能用。

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

写入数据

注意,这里用的是 suspend函数,必须在协程里调用。

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

读取数据

读取返回的是 Flow,可以实时监听变化。你想想看,以前用SP监听变化多麻烦?还要反注册。

val userNameFlow: Flow<String?> = context.dataStore.data.map { preferences ->
    preferences[stringPreferencesKey("user_name")]
}
💡 小技巧: 如果你只需要一次性的读取,可以用 first() 操作符。但大多数场景下,用Flow更优雅。

Proto DataStore:当数据变得复杂

如果你的配置项很多,或者有嵌套结构,比如用户信息包含姓名、年龄、地址,那用Preferences DataStore就会很痛苦——你得手动拼接Key。

这时候,Proto DataStore 就派上用场了。它基于Protocol Buffers,强类型、结构清晰。

定义proto文件

app/src/main/proto/ 目录下创建 user.proto

syntax = "proto3";

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

message UserPreferences {
    string user_name = 1;
    int32 age = 2;
    bool is_logged_in = 3;
}

添加protobuf插件

// 根build.gradle
plugins {
    id "com.google.protobuf" version "0.9.1" apply false
}

// app/build.gradle
plugins {
    id "com.google.protobuf"
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:3.21.7" }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java { option 'lite' }
            }
        }
    }
}

创建DataStore实例

val Context.userDataStore: DataStore<UserPreferences> by dataStore(
    fileName = "user.pb",
    serializer = UserPreferencesSerializer
)

自定义Serializer

嗯,这里要注意,Serializer是必须实现的。它告诉DataStore怎么序列化和反序列化。

object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences
        get() = UserPreferences.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserPreferences {
        return UserPreferences.parseFrom(input)
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        t.writeTo(output)
    }
}

写入与读取

// 写入
suspend fun updateUser(name: String, age: Int) {
    context.userDataStore.updateData { current ->
        current.toBuilder()
            .setUserName(name)
            .setAge(age)
            .build()
    }
}

// 读取
val userFlow: Flow<UserPreferences> = context.userDataStore.data
🎯 核心优势: Proto DataStore 保证了类型安全。如果你改了proto字段,编译期就会报错,不会等到运行时才崩溃。这一点,我在团队里推行后,线上bug减少了至少30%。

实战:键值对存储的完整示例

我们做一个简单的“用户设置”存储,包含主题、语言、通知开关。

1. 定义Key

object SettingsKeys {
    val THEME = stringPreferencesKey("theme")
    val LANGUAGE = stringPreferencesKey("language")
    val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
}

2. 封装Repository

class SettingsRepository(private val dataStore: DataStore<Preferences>) {

    val settingsFlow: Flow<Settings> = dataStore.data.map { prefs ->
        Settings(
            theme = prefs[THEME] ?: "light",
            language = prefs[LANGUAGE] ?: "zh",
            notificationsEnabled = prefs[NOTIFICATIONS_ENABLED] ?: true
        )
    }

    suspend fun updateTheme(theme: String) {
        dataStore.edit { it[THEME] = theme }
    }

    suspend fun updateLanguage(language: String) {
        dataStore.edit { it[LANGUAGE] = language }
    }

    suspend fun toggleNotifications(enabled: Boolean) {
        dataStore.edit { it[NOTIFICATIONS_ENABLED] = enabled }
    }
}

data class Settings(
    val theme: String,
    val language: String,
    val notificationsEnabled: Boolean
)

3. 在ViewModel中使用

class SettingsViewModel(private val repository: SettingsRepository) : ViewModel() {

    val settings: StateFlow<Settings> = repository.settingsFlow
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Settings("light", "zh", true))

    fun setTheme(theme: String) {
        viewModelScope.launch {
            repository.updateTheme(theme)
        }
    }
}
💡 为什么用stateIn? 因为Flow是冷流,每次订阅都会重新执行。用stateIn可以转为热流,并且配合WhileSubscribed,在屏幕旋转时不会重新订阅,性能更好。

知识体系总览

下面这张图,帮你理清DataStore的整个脉络。

DataStore 知识体系 Jetpack DataStore Preferences DataStore Proto DataStore 键值对存储 Flow响应式 类型安全 事务写入 简单配置 → Preferences | 复杂结构 → Proto

总结

DataStore 不是银弹,但它确实解决了SP的很多痛点。我个人建议:

  • 新项目直接上DataStore,别犹豫
  • 老项目如果SP用得不多,可以逐步迁移
  • 如果数据量很大(比如几百个Key),考虑用Room

记住,技术选型没有绝对的对错,但选择更现代、更安全的方案,总是没错的。


公众号:蓝海资料掘金营,微信deep3321