15、DataStore:DataStore与SharedPreferences对比、Preferences DataStore、Proto DataStore

说实话,Android 里做本地轻量数据存储,大家第一反应肯定是 SharedPreferences。我早年做项目时也一直用它,直到有一次在线上遇到了 ANR 问题,追查下来发现是 SP 在主线程读写导致的。嗯,从那时候起,我就开始认真研究 DataStore 了。

这一节,咱们就把 DataStore 彻底聊透。我会从它和 SharedPreferences 的对比讲起,然后分别介绍 Preferences DataStore 和 Proto DataStore 的用法。你跟着走一遍,以后项目里该用哪个,心里就有数了。

DataStore 与 SharedPreferences 的对比

先看一张对比表,这样最直观:

对比维度 SharedPreferences DataStore
线程安全 不保证,容易 ANR 基于 Flow,异步安全
数据格式 仅支持基本类型 + String Preferences(键值对)或 Proto(强类型)
类型安全 无,需要手动转型 Proto DataStore 完全类型安全
错误处理 无,读写异常直接崩溃 抛出 IOException,可捕获处理
数据迁移 需要手动写迁移逻辑 内置迁移支持
依赖注入 通常用静态方法获取 天然支持 DI(如 Hilt)

我个人习惯是:新项目一律用 DataStore,老项目如果只是简单存几个 flag,SP 还能凑合用。但如果你要存用户配置、主题偏好、登录状态这类频繁读写的数据,我建议你直接上 DataStore。

注意:SharedPreferences 的 apply() 虽然是异步写磁盘,但 commit() 是同步的。而且 SP 在第一次加载时会全量读入内存,如果文件很大,主线程就会卡住。我曾经在一个日活百万的 App 里排查 ANR,发现就是 SP 文件膨胀到了 200KB 导致的。

Preferences DataStore

Preferences DataStore 是最简单的入门方式。它跟 SP 一样是键值对,但底层用 Flow 实现,天然异步。

添加依赖

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

创建 DataStore

我一般会在顶层扩展属性里创建,这样全局只有一个实例:

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

写入数据

suspend fun saveThemeMode(context: Context, isDark: Boolean) {
    context.dataStore.edit { preferences ->
        preferences[PreferencesKeys.THEME_MODE] = isDark
    }
}

object PreferencesKeys {
    val THEME_MODE = booleanPreferencesKey("theme_mode")
    val USER_NAME = stringPreferencesKey("user_name")
    val LOGIN_COUNT = intPreferencesKey("login_count")
}

你看,写起来跟 SP 很像,但 edit 是在协程里执行的,不会卡主线程。

读取数据

val themeModeFlow: Flow<Boolean> = context.dataStore.data
    .map { preferences ->
        preferences[PreferencesKeys.THEME_MODE] ?: false
    }

这里返回的是 Flow,你可以用 collect 或者 asLiveData 来观察变化。数据一变,UI 自动更新。

小技巧:如果你只想读一次,可以用 dataStore.data.first()。但大多数场景下,用 Flow 做响应式更新更优雅。

Proto DataStore

Preferences DataStore 虽然简单,但有个问题:键值对没有类型约束。你存的时候是 String,取的时候可能忘了转,就出 bug 了。Proto DataStore 就是来解决这个问题的。

它基于 Protocol Buffers(protobuf),你需要先定义一个 schema 文件,然后生成对应的 Java/Kotlin 类。说白了,就是给数据加了个「合同」,类型不对编译期就报错。

定义 proto 文件

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

syntax = "proto3";

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

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

添加依赖和插件

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

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

dependencies {
    implementation "androidx.datastore:datastore:1.0.0"
    implementation "com.google.protobuf:protobuf-javalite:3.21.7"
}

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

创建 Proto DataStore

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

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 updateDarkMode(context: Context, isDark: Boolean) {
    context.userPrefsDataStore.updateData { currentPrefs ->
        currentPrefs.toBuilder()
            .setIsDarkMode(isDark)
            .build()
    }
}

// 读取
val darkModeFlow: Flow<Boolean> = context.userPrefsDataStore.data
    .map { prefs -> prefs.isDarkMode }

看到没?这里读写都是通过生成的 UserPreferences 类来操作的,字段名、类型都是确定的。你不可能写错 key,也不可能取出来类型不对。

核心结论:Preferences DataStore 适合简单场景(比如存几个 flag),Proto DataStore 适合数据结构复杂或需要多人协作的项目。我个人在团队项目里一律用 Proto,因为代码可读性和维护性都好太多了。

知识体系总览

下面这张图帮你理清 DataStore 的整体脉络:

DataStore 知识体系 DataStore Preferences DataStore Proto DataStore 键值对存储 Flow 异步读写 适合简单配置 强类型 schema 编译期类型检查 适合复杂数据结构 替代 SharedPreferences,解决 ANR 和类型安全问题

避坑指南

最后分享几个我踩过的坑:

  • 不要在主线程 collect:DataStore 的 data Flow 是在 IO 线程发射的,如果你在 Activity 里直接 collect 并更新 UI,记得用 lifecycleScope.launch 配合 Dispatchers.Main
  • 文件损坏处理:DataStore 在读写时如果遇到文件损坏,会抛出 IOException。我建议在 collect 时用 try-catch 包一下,或者用 catch 操作符给个默认值。
  • 迁移成本:从 SP 迁移到 DataStore 可以用 SharedPreferencesMigration,但要注意 key 的映射关系。我曾经因为 key 名写错,导致用户数据全部丢失,后来加了单元测试才放心。
我的建议:如果你现在还在用 SharedPreferences,不妨花半天时间把核心配置迁移到 DataStore。别等到线上出 ANR 了再改,那时候就晚了。

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