DataStore存储:告别SharedPreferences,拥抱新时代

说实话,在Android开发中做本地轻量数据存储,大家第一反应肯定是SharedPreferences。但如果你经历过SP的坑——比如ANR、数据丢失、类型不安全——你就会明白为什么Google要推出DataStore。今天我就带你彻底搞懂DataStore,从对比到实战,一步到位。

核心结论:DataStore是SharedPreferences的官方替代品,基于协程和Flow,天生异步,类型安全,且支持两种数据格式。

DataStore vs SharedPreferences:一场降维打击

先别急着学DataStore怎么用,我们得先知道它好在哪里。我当年维护过一个老项目,SharedPreferences里存了上百个key,每次读取都提心吊胆——万一主线程读SP卡了,用户直接看到白屏。这就是SP最大的痛点:同步读写阻塞主线程。

对比维度 SharedPreferences DataStore
读写方式 同步(可能ANR) 异步(协程+Flow)
类型安全 不保证(存String取Int会崩溃) 完全类型安全
数据一致性 写入可能丢失 事务性写入,保证一致
异常处理 Flow自带异常处理
支持数据格式 简单键值对 键值对 + 类型化对象

你看,SP几乎每一项都处于劣势。尤其是数据一致性——我曾经遇到过用户反馈「设置明明保存了,重启后却丢了」,查了半天发现是SP在写入过程中进程被杀。DataStore用事务性写入彻底解决了这个问题。

我的建议:新项目直接放弃SharedPreferences,全部用DataStore。老项目迁移也不难,后面我会讲迁移方案。

Preferences DataStore vs Proto DataStore

DataStore有两种实现方式,很多人一开始会混淆。简单来说:

  • Preferences DataStore:类似SP的键值对,适合存简单配置(开关、字符串、数字)
  • Proto DataStore:基于Protocol Buffers,需要定义schema,适合存结构化数据

怎么选?我个人的经验是:如果你的数据不超过5个key,且都是基本类型,用Preferences就够了。但如果你要存用户信息、复杂配置对象,Proto DataStore更合适——它自带类型约束,不会出现「取出来发现类型不对」的尴尬。

来看一个Preferences DataStore的典型用法:

// 定义扩展属性
val Context.settingsDataStore by preferencesDataStore(name = "settings")

// 读取
val isDarkMode: Flow<Boolean> = context.settingsDataStore.data
    .map { preferences ->
        preferences[PreferencesKeys.IS_DARK_MODE] ?: false
    }

// 写入
suspend fun setDarkMode(enabled: Boolean) {
    context.settingsDataStore.edit { preferences ->
        preferences[PreferencesKeys.IS_DARK_MODE] = enabled
    }
}

而Proto DataStore需要先定义proto文件:

// user_preferences.proto
syntax = "proto3";

message UserPreferences {
    bool is_dark_mode = 1;
    string user_name = 2;
    int32 font_size = 3;
}

然后生成代码,再创建DataStore:

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

// 读取
val userPreferences: Flow<UserPreferences> = context.userPreferencesDataStore.data

// 写入
suspend fun updateUserName(name: String) {
    context.userPreferencesDataStore.updateData { preferences ->
        preferences.toBuilder()
            .setUserName(name)
            .build()
    }
}

注意:Proto DataStore需要引入protobuf插件和依赖,构建配置稍复杂。但一旦配好,用起来非常爽——你再也不用担心key拼写错误了。

异步读写与异常处理

DataStore的所有操作都是异步的,这意味着你必须运行在协程作用域内。很多人刚接触时会问:「那我怎么在ViewModel里用?」很简单,用viewModelScope或者lifecycleScope

但异步也带来了异常处理的问题。DataStore的data是一个Flow,它可能因为I/O错误、序列化失败等原因抛出异常。如果不处理,整个Flow会终止。

我推荐的做法是使用catch操作符:

val isDarkMode: Flow<Boolean> = context.settingsDataStore.data
    .catch { exception ->
        // 记录日志,返回默认值
        Log.e("DataStore", "读取失败", exception)
        emit(emptyPreferences())
    }
    .map { preferences ->
        preferences[PreferencesKeys.IS_DARK_MODE] ?: false
    }

写入操作也可能失败,比如磁盘空间不足。这时editupdateData会抛出异常。我的习惯是在Repository层统一捕获:

suspend fun setDarkMode(enabled: Boolean): Result<Unit> {
    return try {
        context.settingsDataStore.edit { preferences ->
            preferences[PreferencesKeys.IS_DARK_MODE] = enabled
        }
        Result.success(Unit)
    } catch (e: Exception) {
        Log.e("DataStore", "写入失败", e)
        Result.failure(e)
    }
}

避坑指南:我曾经在写入频繁的场景下忘记加catch,结果用户反馈「设置保存不了」——其实是因为某次写入异常导致后续所有写入都失败了。所以,永远不要忽略DataStore的异常处理

DataStore与Room的选型

这是新手最容易纠结的问题。其实很简单:

  • DataStore:适合存少量、简单的配置数据(几十个key以内)
  • Room:适合存大量、结构化、需要查询的数据(几百条以上)

你想想看,如果你用Room存一个「是否开启暗黑模式」的开关,是不是有点杀鸡用牛刀?Room需要定义Entity、DAO、Database,还要处理数据库升级,太复杂了。反过来,如果你要存用户的1000条历史记录,用DataStore也不合适——它不支持按条件查询,每次都要全量读取。

我个人的选型原则是:

  1. 数据量 < 50个key,且不需要查询 → DataStore
  2. 数据量 > 100条,或需要SQL查询 → Room
  3. 两者之间 → 看业务复杂度,简单用DataStore,复杂用Room

一个项目里可以同时用DataStore和Room。比如用DataStore存用户偏好设置,用Room存离线缓存数据。它们不冲突,反而各司其职。

知识体系总览

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

DataStore vs SharedPreferences 两种实现方式 异步读写与异常 vs Room 选型 Preferences DataStore Proto DataStore 协程 + Flow 异常处理 catch 简单配置数据 结构化对象

这张图把DataStore的核心知识点串起来了。你可以看到,它主要解决四个问题:替代SP、两种实现、异步异常、与Room的选型。每个分支往下都有具体的子知识点。

最后说一句:DataStore的学习曲线其实很低,只要你熟悉协程和Flow,半小时就能上手。但真正用好它,需要理解它的设计哲学——异步、类型安全、事务性。记住这三点,你就能写出健壮的本地存储代码。

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