9、DataStore KTX:使用DataStore替代SharedPreferences,KTX风格的读写操作

说实话,SharedPreferences 陪伴我们 Android 开发者很多年了。但每次用它,我心里都有点不踏实。同步读写可能卡 UI,apply 异步又不知道什么时候写完,多进程更是噩梦。Google 自己也明白,所以推出了 DataStore 来替代它。

今天我们就聊聊 DataStore 的 KTX 写法。说白了,就是用 Kotlin 的协程和 Flow,让数据存储变得优雅又安全。

为什么我建议你换掉 SharedPreferences

先说说我在项目里踩过的坑。有一次线上反馈说用户设置的主题色经常丢失,查了半天,原来是多线程同时读写 SharedPreferences 导致的。你想想看,一个简单的配置项,居然能搞出数据不一致,这谁受得了?

DataStore 解决了两个核心问题:

  • 异步安全:所有操作都在协程里跑,不会阻塞主线程
  • 类型安全:用 Kotlin 的序列化,不用再手写 key-value 字符串

而且 DataStore 有两种口味:

类型 适用场景 特点
Preferences DataStore 简单键值对(类似 SP) 轻量,无需定义 schema
Proto DataStore 结构化数据 强类型,需要定义 protobuf

我个人习惯,如果只是存几个配置项,用 Preferences DataStore 就够了。要是数据复杂,比如用户信息、缓存列表,那就上 Proto DataStore。

Preferences DataStore 的 KTX 写法

先加依赖:

implementation("androidx.datastore:datastore-preferences:1.0.0")

然后定义一个顶层属性,用委托创建 DataStore 实例:

val Context.settingsDataStore by preferencesDataStore(
    name = "settings"
)

读写操作呢?看代码:

// 写数据 - 用 edit 块
suspend fun saveUserName(context: Context, name: String) {
    context.settingsDataStore.edit { preferences ->
        preferences[stringPreferencesKey("user_name")] = name
    }
}

// 读数据 - 返回 Flow
val userNameFlow: Flow<String> = context.settingsDataStore.data
    .map { preferences ->
        preferences[stringPreferencesKey("user_name")] ?: ""
    }

嗯,这里要注意:edit 是挂起函数,必须在协程里调用。而 data 返回的是 Flow,天然支持响应式观察。

核心区别:SharedPreferences 是「拉」数据,DataStore 是「推」数据。数据一变,Flow 自动发射新值。

Proto DataStore 的 KTX 写法

如果数据有结构,比如用户信息包含姓名、年龄、头像,用 Preferences 就得写一堆 key,太累了。这时候 Proto DataStore 就派上用场了。

首先定义 protobuf 文件:

syntax = "proto3";

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

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

然后生成代码,创建 DataStore:

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

读写操作:

// 写数据
suspend fun updateUser(context: Context, name: String, age: Int) {
    context.userPreferencesDataStore.updateData { currentPreferences ->
        currentPreferences.toBuilder()
            .setUserName(name)
            .setAge(age)
            .build()
    }
}

// 读数据
val userFlow: Flow<UserPreferences> = context.userPreferencesDataStore.data

你看,读出来直接就是强类型的对象,不用再手动解析字符串了。我曾经在一个老项目里,用 SharedPreferences 存 JSON 字符串,每次读都要 Gson 解析,写还要序列化,烦得很。换成 Proto DataStore 后,代码干净多了。

避坑指南:我曾经踩过的坑

讲几个我实际遇到的问题,你遇到了可以少走弯路。

坑一:DataStore 的异常处理

DataStore 的 data Flow 可能会抛出异常,比如文件损坏。建议用 catch 处理:

context.settingsDataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }
    .map { ... }

坑二:不要在非协程环境调用 edit

我刚开始用的时候,在 RecyclerView 的点击事件里直接调了 edit,结果编译报错。后来才意识到,必须用 lifecycleScope.launch 或者 viewModelScope.launch 包一层。

坑三:DataStore 是单例的

同一个文件名的 DataStore 在整个进程里只有一个实例。如果你在多个地方创建同名 DataStore,会抛出异常。所以建议用顶层委托,全局共享。

知识体系总览

下面这张图帮你理清 DataStore 的核心脉络:

DataStore KTX 知识体系 Preferences DataStore Proto DataStore 轻量键值对存储 edit {} 挂起函数写 data Flow 响应式读 强类型结构化数据 updateData {} 更新 data Flow 直接读对象 共同优势:协程安全 · 类型安全 · 响应式

实战:封装一个优雅的 DataStore 工具类

最后,分享一个我项目里用的封装思路。把 key 定义和读写方法集中管理,用起来特别顺手:

object SettingsKeys {
    val DARK_MODE = booleanPreferencesKey("dark_mode")
    val LANGUAGE = stringPreferencesKey("language")
    val FONT_SIZE = intPreferencesKey("font_size")
}

class SettingsRepository(private val context: Context) {
    
    val darkMode: Flow<Boolean> = context.settingsDataStore.data
        .map { it[SettingsKeys.DARK_MODE] ?: false }
    
    val language: Flow<String> = context.settingsDataStore.data
        .map { it[SettingsKeys.LANGUAGE] ?: "zh" }
    
    suspend fun setDarkMode(enabled: Boolean) {
        context.settingsDataStore.edit {
            it[SettingsKeys.DARK_MODE] = enabled
        }
    }
    
    suspend fun setLanguage(lang: String) {
        context.settingsDataStore.edit {
            it[SettingsKeys.LANGUAGE] = lang
        }
    }
}

这样在 ViewModel 里调用就非常清爽了:

class SettingsViewModel(application: Application) : AndroidViewModel(application) {
    
    private val repository = SettingsRepository(application)
    
    val darkMode = repository.darkMode.stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000),
        false
    )
    
    fun toggleDarkMode() {
        viewModelScope.launch {
            val current = repository.darkMode.first()
            repository.setDarkMode(!current)
        }
    }
}

你看,整个流程下来,没有 SharedPreferences 的同步阻塞,没有 apply 的不可控,也没有 key 拼写错误的风险。DataStore + KTX 的组合,说白了就是让数据存储回归简单。

我个人觉得,这是 Android 官方这几年做得最靠谱的 API 改进之一。如果你还在用 SharedPreferences,真的可以试试换过来了。


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