13、Flow与DataStore:DataStore的Flow支持、替代SharedPreferences、类型安全的数据存储

说实话,每次看到项目里还在用SharedPreferences存复杂对象,我都替那帮开发者捏把汗。我自己早期维护过一个日活百万的App,SharedPreferences里塞了上百个key-value,每次读写都卡主线程,ANR报告里一半都是它惹的祸。后来换成DataStore,世界清净了。

这一章,我们就来聊聊DataStore。它本质上是Google官方推出的、基于Flow的、类型安全的数据存储方案。说白了,就是用来替代SharedPreferences的。

核心结论:DataStore + Flow = 响应式、协程安全、类型安全的本地键值存储。别再写SharedPreferences了。

13.1 为什么SharedPreferences该退休了?

SharedPreferences有几个硬伤,我简单列一下:

  • 同步读写阻塞主线程——你调用getString()时,它可能触发磁盘IO,直接卡UI。
  • 没有类型安全——存的时候是String,取的时候可能强转失败,运行时崩。
  • 不支持响应式监听——虽然有OnSharedPreferenceChangeListener,但用起来极其别扭,容易内存泄漏。
  • apply()的异步不可控——你以为写完了,其实还在排队,进程被杀就丢数据。

嗯,这里要注意:SharedPreferences在Android 12以上已经被标记为@Deprecated了。官方态度很明确——用DataStore。

13.2 DataStore的两种口味

DataStore分两种:Preferences DataStoreProto DataStore

特性 Preferences DataStore Proto DataStore
数据类型 仅支持Int、Long、Float、Double、Boolean、String、Set<String> 支持自定义对象(通过Protocol Buffers定义)
类型安全 弱(运行时类型检查) 强(编译时类型检查)
依赖 仅需DataStore核心库 需要Protobuf编译插件
适用场景 简单键值对(如用户设置、开关状态) 复杂结构化数据(如用户配置、缓存模型)

我个人习惯:如果只是存几个开关、token、用户名,用Preferences DataStore就够了。如果涉及嵌套对象、枚举、列表,果断上Proto DataStore。

13.3 Preferences DataStore实战

先加依赖:

// build.gradle.kts (Module)
implementation("androidx.datastore:datastore-preferences:1.1.1")

然后创建一个DataStore实例。我一般把它定义在顶层扩展属性里:

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

读写操作全部通过Flow完成。举个例子,存一个用户是否登录的标志:

// 定义key
private val IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")

// 读取——返回Flow
val Context.isLoggedInFlow: Flow<Boolean>
    get() = settingsDataStore.data.map { preferences ->
        preferences[IS_LOGGED_IN] ?: false
    }

// 写入——挂起函数
suspend fun Context.setLoggedIn(loggedIn: Boolean) {
    settingsDataStore.edit { preferences ->
        preferences[IS_LOGGED_IN] = loggedIn
    }
}

你看,读取返回的是Flow,天然支持响应式。UI层只要collect这个Flow,数据一变,界面自动更新。SharedPreferences能做到吗?做不到。

小技巧:如果你不想每次collect都重新发射数据,可以用.distinctUntilChanged()过滤重复值。我在项目中经常这么干,避免不必要的UI刷新。

13.4 Proto DataStore:类型安全的终极方案

Preferences DataStore虽然简单,但有个问题——key是字符串,value的类型你得自己记。万一写错了key或者类型,运行时才暴露。

Proto DataStore用Protocol Buffers定义schema,编译时生成Java/Kotlin类,类型安全拉满。

首先,定义proto文件:

// app/src/main/proto/user_settings.proto
syntax = "proto3";

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

message UserSettings {
    bool is_logged_in = 1;
    string username = 2;
    int32 theme_mode = 3; // 0: light, 1: dark, 2: system
}

然后配置Protobuf编译插件(略,官方文档有详细步骤)。编译后会生成UserSettings类。

创建DataStore:

val Context.userSettingsDataStore by dataStore(
    fileName = "user_settings.pb",
    serializer = UserSettingsSerializer
)

Serializer需要自己实现:

object UserSettingsSerializer : Serializer<UserSettings> {
    override val defaultValue: UserSettings
        get() = UserSettings.getDefaultInstance()

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

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

读写操作:

// 读取
val Context.userSettingsFlow: Flow<UserSettings>
    get() = userSettingsDataStore.data

// 写入
suspend fun Context.updateUsername(newName: String) {
    userSettingsDataStore.updateData { currentSettings ->
        currentSettings.toBuilder()
            .setUsername(newName)
            .build()
    }
}

注意updateData是事务性的——如果写入过程中发生异常,整个操作会回滚,不会出现半写状态。这一点比SharedPreferences的apply()靠谱太多了。

我曾经踩过的坑:Proto DataStore的defaultValue一定要给合理值。如果你给了一个空对象,而UI层直接读取嵌套字段,可能拿到默认值0或空字符串,导致逻辑判断出错。建议在defaultValue里设置合理的业务默认值。

13.5 核心知识体系

下面这张图总结了DataStore与Flow结合的核心脉络:

DataStore + Flow 核心知识体系 DataStore 两种类型 Preferences Proto 核心特性 ✅ 基于Flow,响应式 ✅ 协程安全,非阻塞 ✅ 类型安全(Proto更强) 最佳实践 distinctUntilChanged 合理defaultValue 事务性写入 替代 SharedPreferences 解决:主线程阻塞 · 类型不安全 · 无响应式 · 异步不可控

13.6 在ViewModel中优雅使用

有了Flow,ViewModel里的代码可以写得很干净。我一般这样组织:

class SettingsViewModel(
    private val context: Context
) : ViewModel() {

    val isLoggedIn: StateFlow<Boolean> = context.isLoggedInFlow
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = false
        )

    fun toggleLogin() {
        viewModelScope.launch {
            val current = context.isLoggedInFlow.first()
            context.setLoggedIn(!current)
        }
    }
}

这里用了stateIn把冷Flow转成热StateFlow,UI层直接观察。注意WhileSubscribed(5000)——当界面不可见时,5秒内不取消订阅,避免频繁重建。

个人经验:如果你在多个ViewModel里都要读同一个DataStore,建议封装成一个Repository,统一管理DataStore实例。不要在每个ViewModel里都创建一遍,容易造成资源浪费。

13.7 迁移指南:从SharedPreferences到DataStore

如果你想把老项目里的SharedPreferences迁移过来,Google官方提供了一个迁移工具:

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

它会自动把SharedPreferences里的数据读到DataStore里,然后删除旧文件。整个过程对用户无感。

不过要注意:迁移是一次性的。如果旧SharedPreferences里有大量数据,第一次启动时可能会稍微慢一点。我建议在应用启动时做个异步预迁移,别让用户等。

警告:迁移过程中不要同时读写SharedPreferences和DataStore,否则可能出现数据不一致。迁移完成后,旧文件会被标记为删除,但不会立即物理删除,所以不要依赖旧文件是否存在来判断迁移状态。

13.8 总结

DataStore + Flow的组合,说白了就是Android本地存储的现代答案。它解决了SharedPreferences几乎所有痛点:主线程阻塞、类型不安全、无响应式、异步不可控。

我个人建议:新项目直接上Proto DataStore,老项目逐步迁移。别犹豫,越早换越省心。

嗯,这一章就到这里。记住:数据存储是应用的基石,选对方案,后面少踩很多坑。


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