前言:为什么要写这篇文章
在现代 Android 开发中,本地数据持久化是几乎所有应用的核心需求。Jetpack 提供了两套官方推荐的存储方案:
- DataStore:轻量级键值对存储,推荐替代 SharedPreferences
- Room:强大且类型安全的 SQLite 封装数据库
本文从实际开发出发,与 Kotlin 的 Flow 结合后,可以构建出响应式、简洁、高效的本地数据架构。最近看之前项目代码,发现不少小伙伴并不知道两者可以结合使用。所以本文将深入讲解如何将 DataStore 和 Room 与 Flow 高效组合使用。
Demo将尽可能模拟实际开发,github地址:github.com/tcyang12345... ,选择"持久化测试"。需要的小伙伴可搬到生产代码中去。
一、为什么需要Flow
传统的 LiveData 虽然好用,但在以下场景存在局限:
- 无法天然表达"单次事件"(需要额外处理一次性事件)
- 生命周期感知强,但无法在非 UI 层(如 Repository、UseCase)中使用
- 不支持背压、复杂的流操作(map、filter、combine、flatMapConcat 等)
而 Flow 是纯 Kotlin 的响应式流,完全运行在协程中,具备:
- 冷流特性:只有被收集时才执行
- 强大的操作符支持
- 天然支持背压
- 可在任意协程作用域中使用
因此,用 Flow 作为统一的数据流动载体,是目前最推荐的实践。关于Flow相关介绍,可以查看我的这一篇文章juejin.cn/post/754740...
本文主要讲述两者的使用,以及和Flow的结合使用,Room的SQL语法不作重点讲述。
二、DataStore + Flow:设置数据最佳组合
添加第三方依赖,包含 Preferences和Proto
kotlin
// 数据存储datastore
implementation("androidx.datastore:datastore-preferences:1.0.0")
// 或者使用 Proto DataStore
implementation("androidx.datastore:datastore-core:1.0.0")
implementation("com.google.protobuf:protobuf-javalite:3.25.0")
1. Preferences DataStore(键值对)
kotlin
object PreferencesKeys {
val THEME_MODE = preferencesKey<String>("theme_mode")
val USER_TOKEN = preferencesKey<String>("user_token")
val HAS_NEW_GUIDE = preferencesKey<Boolean>("has_new_guide")
}
object DataStoreManager {
private const val DATA_STORE_NAME = "app_settings"
private val Context.pfsDataStore: DataStore<Preferences> by preferencesDataStore(
name = DATA_STORE_NAME
)
// 此处是proto store扩展方法,后续使用
val Context.userProtoDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb", //注意后缀,虽不强制,但最好遵循规范
serializer = UserSettingsSerializer,
corruptionHandler = ReplaceFileCorruptionHandler (
produceNewData = { UserSettings.getDefaultInstance()}
)
)
val context get() = MyApp.instance
/**
* 主题模式数据流,用于监听主题设置的变化
* @return 返回一个Flow<String>,表示当前的主题模式(默认为"light")
*/
val themeModeFlow: Flow<String> = context.pfsDataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[PreferencesKeys.THEME_MODE] ?: "light"
}
/**
* 用户Token数据流,用于监听用户Token的变化
* @return 返回一个Flow<String>,表示当前用户的Token(默认为空字符串)
*/
val userTokenFlow: Flow<String> = context.pfsDataStore.data.catch { ex ->
if (ex is IOException) {
emit(emptyPreferences())
} else {
throw ex
}
}.map { preferences ->
preferences[PreferencesKeys.USER_TOKEN] ?: ""
}
/**
* 监听所有的数据Preferences Datastore
* 包括主题模式、用户Token和新手引导状态三个字段的组合数据流
* @return 返回一个Flow<Triple<String, String, Boolean>>,
* 第一个元素为主题模式,第二个为用户Token,第三个为新手引导状态
*/
val userSettingsFlow: Flow<Triple<String, String, Boolean>> = context.pfsDataStore.data.catch { ex ->
if (ex is IOException) {
emit(emptyPreferences())
} else {
throw ex
}
}.map { preferences ->
Triple(
preferences[PreferencesKeys.THEME_MODE] ?: "light",
preferences[PreferencesKeys.USER_TOKEN] ?: "",
preferences[PreferencesKeys.HAS_NEW_GUIDE] ?: false
)
}
suspend fun saveThemeMode(mode: String) {
context.pfsDataStore.edit { preferences ->
preferences[PreferencesKeys.THEME_MODE] = mode
}
}
suspend fun saveToken(token: String) {
context.pfsDataStore.edit { settings ->
settings[PreferencesKeys.USER_TOKEN] = token
}
}
suspend fun clearToken() {
context.pfsDataStore.edit { it.clear() }
}
}
使用方式:
在Viewmodel中
kotlin
/*----------以下是preferences datastore----------------*/
/**
* 将用户设置数据流转换为状态流
*
* 该函数将DataStoreManager中的用户设置流转换为StateFlow,以便在ViewModel中使用。
* 当有订阅者时开始收集数据,无订阅者时延迟5秒停止收集以节省资源。
*
* @param scope 作用域,使用viewModelScope确保生命周期安全
* @param started 控制流的启动和停止策略,WhileSubscribed(5000)表示当有订阅时才收集,无订阅时延迟5秒停止
* @param initialValue 初始值,当还没有数据时使用的默认设置:主题为"light",语言为空字符串,布尔值为false
* @return 返回一个StateFlow,包含Triple类型的用户设置数据(主题、语言、布尔标志)
*/
val themeModeFlow: Flow<String> = DataStoreManager.themeModeFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = "light"
)
/**
* 所有设置监听
*/
val allSettingFlow: Flow<Triple<String, String, Boolean>> = DataStoreManager.userSettingsFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Triple("light", "", false)
)
在UI界面(如Activity中)
kotlin
lifecycleScope.launch {
viewModel.allSettingFlow.collect { tripe ->
tvAllSetting?.text =
"themeMode: ${tripe.first}, userToken: ${tripe.second}, hasNewGuide: ${tripe.third}"
}
}
2. Proto DataStore(结构化自定义对象支持)
- 定义 schema:
proto
// proto/user_prefs.proto
syntax = "proto3";
option java_package = "com.example.datastore";
option java_multiple_files = true;
message UserSettings {
string user_id = 1;
string nickname = 2;
bool is_vip = 3;
int32 login_count = 4;
}
- "Build" -> "Make Project"生成 "UserSetting.java"
- 增加序列化和反序列化工具类 "UserSettingsSerializer.kt"
kotlin
/**
* UserPrefsSerializer 是一个用于序列化和反序列化 UserPrefs 对象的工具类
* 实现了 Serializer 接口,提供默认值、读取和写入功能
*/
object UserSettingsSerializer: Serializer<UserSettings> {
/**
* 获取 UserPrefs 的默认值
* 当无法从输入流中解析数据时,将返回此默认值
*
* @return 默认的 UserPrefs 对象,包含预设的用户信息
*/
override val defaultValue: UserSettings
get() = UserSettings.newBuilder().build()
/**
* 从输入流中读取并解析 UserPrefs 对象
* 如果解析过程中发生异常,则返回默认值
*
* @param input 输入流,包含序列化的 UserPrefs 数据
* @return 解析成功的 UserPrefs 对象,或在失败时返回默认值
*/
override suspend fun readFrom(input: InputStream): UserSettings {
// 尝试从输入流解析 UserPrefs 对象,如果失败则返回默认值
return kotlin.runCatching {
return UserSettings.parseFrom(input)
}.onFailure {
return defaultValue
}.getOrNull() ?: defaultValue
}
/**
* 将 UserPrefs 对象序列化并写入到输出流中
*
* @param t 要序列化的 UserPrefs 对象
* @param output 输出流,用于写入序列化的数据
*/
override suspend fun writeTo(t: UserSettings, output: OutputStream) {
t.writeTo(output)
}
}
- 在 DataStoreManager 中添加扩展方法,当然你也可以在其他地方添加。
kotlin
// 此处是proto store扩展方法
val Context.userProtoDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.db",
serializer = UserSettingsSerializer,
corruptionHandler = ReplaceFileCorruptionHandler (
produceNewData = { UserSettings.getDefaultInstance()}
)
)
- 监听DataStore暴露的Flow 接口
// DataStoreManager.kt
kotlin
private val proDataStore = context.userProtoDataStore
/**
* 获取完整的用户偏好设置数据流。
*/
private val userSettingsFlow: Flow<UserSettings> = proDataStore.data
// 以下为各个字段对应的 Flow 数据流,便于单独监听某个属性的变化
/**
* 用户 ID 的数据流。
*/
val userIdFlow = userSettingsFlow.map { it.userId }
/**
* 是否为 VIP 用户的数据流。
*/
val isVipFlow = userSettingsFlow.map { it.isVip }
此处监听了所有的参数,当时你也可以单独监听某一个参数,删除对应的preferences[PreferencesKeys.HAS_NEW_GUIDE] 即可。
使用方式
使用方式与Preferences DataStore 相似,在Viewmodel和Activity可仿照Preferences DataStore 开发,此处不再赘述。
3.两者的区别(Preferences vs Proto)
数据类型支持
- Preferences DataStore:仅支持基本数据类型,适配存储简单配置信息
- Proto DataStore:支持复杂数据结构,允许嵌套和列表等类型,但需要注意的是:建议不要用来存储复杂数据对象和大批量数据
类型安全
- Preferences :运行时检查,相对容易出错
- Proto :编译时检查,类型安全性更强
性能表现
- Preferences :对简单数据访问更快
- Proto:首次会有极轻微的序列化开销
使用场景
- Preferences :用户设置、主体设置等简单状态
- Proto : 存储复杂的结构化数据
官方建议
Google 自己的 Google I/O 2024 App、Google Home、YouTube 等新项目,已经全部改用 Proto DataStore,连 Preferences 都不迁移了。
4.DataStore自动更新Flow的关键点
| 步骤 | 关键技术点 |
|---|---|
| 数据写入 | 内存缓存 + 写磁盘 + 更新文件 |
| 检测文件变化 | DataStore 通过文件监听(FileObserver/Inotify)检测到文件变更后,重新读取并发射新值 |
| 触发 Flow 更新 | callbackFlow + observer → trySend 新数据 |
| 去重 | distinctUntilChanged() |
| 多进程支持 | 所有进程监听同一个文件路径 |
| 监听Flow变化 | 自动更新UI页面 |
三、Room的基本使用
当你需要存储大量数据和复杂的对象,建议使用Room
1、添加依赖、添加 ksp 插件
kotlin
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.proto)
alias(libs.plugins.ksp) //开启ksp
}
android {
//其他代码请自行补充
kotlinOptions {
jvmTarget = "11"
// 关键:解决 KSP moduleName 报错
freeCompilerArgs += listOf(
"-Xmodule-name=${project.name}" // 或者用 ${project.name}
)
}
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.expandProjection", "true")
}
dependencies {
//其他依赖不在此列出
implementation ("androidx.room:room-runtime:2.6.1")
implementation ("androidx.room:room-ktx:2.6.1" ) // coroutine + Flow 支持
ksp ("androidx.room:room-compiler:2.6.1")
}
libs.version.toml 相关代码如下
kotlin
[plugins]
android-application = { id = "com.android.application", version = "8.7.2" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.0.0" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version = "2.0.0" }
ksp = { id = "com.google.devtools.ksp" , version = "2.0.0-1.0.21"}
注意:Room、kotlin版本、ksp版本存在兼容性关联,可参照上述代码设置。
2、创建实体类(Entity)
基础Dao的写法(注意13行的写法,返回Flow,推荐)
常用注解:
- @Entity:表
- @PrimaryKey:主键
- @ColumnInfo(name = "xxx"):自定义列名
- @Ignore:忽略该字段不存数据库
data/room/UserEntity.kt
kotlin
@Entity(
tableName = "user",
//索引, 提高查询性能
indices =
[
androidx.room.Index(
value = ["email"],
unique = true
)
]
)
data class UserEntity (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val username: String,
val phoneNum: String,
val email: String,
val createTime: Long = System.currentTimeMillis(),
)
3、创建Dao接口(负责操作数据库)
data/room/UserDao.kt
kotlin
@Dao
interface UserDao {
//插入,返回新插入行的id,若id相同,则替换原数据
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(userEntity: UserEntity): Long
//批量插入
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll( userEntity: List<UserEntity>)
//使用Flow 来获取可观察的数据流,当表数据变更时自动发射数据
@Query("SELECT * FROM user ORDER BY createTime DESC")
fun observerAll(): Flow<List<UserEntity>>
//查询
@Query("SELECT * FROM user")
suspend fun queryAll(): List<UserEntity>
//查询指定用户
@Query("SELECT * FROM user WHERE username = :username")
suspend fun queryByUsername(username: String): UserEntity?
//更新,也可以使用@Query注解
@Update
suspend fun update(userEntity: UserEntity)
//删除,推荐用Query注解代替@Delete注解
@Query("DELETE FROM user")
suspend fun deleteAll()
//删除指定用户
@Query("DELETE FROM user WHERE email = :email")
suspend fun deleteByEmail(email: String)
// 使用 @Delete 注解的方式
@Delete
suspend fun delete(userEntity: UserEntity)
}
3、创建数据库类
data/room/AppDataBase.kt
kotlin
/**
* AppDatabase 是一个抽象类,继承自 RoomDatabase,用于定义应用程序的数据库。
* 它包含了数据库的配置信息以及获取 DAO 实例的方法。
*/
@Database(entities = [UserEntity::class], version = 1, exportSchema = true)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
/**
* 获取 UserDao 实例的抽象方法。
*
* @return UserDao 返回用户数据访问对象实例
*/
abstract fun userDao(): UserDao
companion object {
/**
* 数据库单例实例,使用 volatile 关键字保证多线程环境下的可见性。
*/
@Volatile
private var INSTANCE: AppDatabase? = null
/**
* 获取 AppDatabase 单例实例的函数。
* 使用双重检查锁定模式确保线程安全和高性能。
*
* @param context 上下文对象,用于构建数据库
* @return AppDatabase 返回数据库实例
*/
fun getInstance(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
/**
* 构建 AppDatabase 实例的私有函数。
* 配置数据库名称并创建数据库构建器。
*
* @param context 上下文对象
* @return AppDatabase 返回新构建的数据库实例
*/
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_db")
// 添加 Migration,升级使用。
//.addMigrations(MIGRATION_1_2)
.build()
}
}
}
4、额外的数据转换(非必选)
data/room/Converters.kt
kotlin
/**
* 时间转换器类,用于在数据库实体和Java Date对象之间进行转换
*/
class Converters {
/**
* 将时间戳转换为Date对象
*
* @param value 时间戳值,以毫秒为单位
* @return 返回对应的Date对象,如果输入为空则返回当前时间的Date对象
*/
@TypeConverter
fun fromTimestamp(value: Long?): Date = value?.let { Date(it) } ?: Date()
/**
* 将Date对象转换为时间戳
*
* @param date 要转换的Date对象
* @return 返回对应的时间戳,以毫秒为单位
*/
@TypeConverter
fun dateToTimestamp(date: Date): Long = date.time
}
完成这几步后,我们在Application 中调用AppDatabase.getInstance(this)后,就创建了一个名为"app_db"的数据库,里面包含了一张名为"user"的表
5、小技巧:如何查看数据库
Android studio:App Inspection ->Database Inspector

6、Room版本升级
随着业务的变化,需要增加字段,那就涉及到数据库版本升级。步骤如下:
- A、调整建表语句,增加列"age" 默认为0
less
@Entity(
tableName = "user",
indices =
[
androidx.room.Index(
value = ["email"],
unique = true
)
]
)
data class UserEntity (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val username: String,
val phoneNum: String,
val email: String,
val age: Int = 0,
val createTime: Long = System.currentTimeMillis(),
)
- B、增加数据库版本号(原版本号X+1)
kotlin
@Database(entities = [UserEntity::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
// ...
}
- C、添加迁移策略(MIGRATION_X_(X+1))
kotlin
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// 执行数据库结构变更SQL, 增加名为age列
db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
- D、数据库构建时添加迁移策略,使用X_(X+1)升级策略
css
Room.databaseBuilder(context, AppDatabase::class.java, "app_db")
.addMigrations(MIGRATION_1_2)
.build()
升级踩坑点:
- 1、新增列时需要注意:
Room默认数据库列必须是NOT NULL,除非显示声明可空。
Int类型必须在新增列时同时声明NOT NULL DEFAULT 0,否则已有旧数据那一行 age 就是 NULL,会和新的实体类定义(age不为空)冲突 → Room 直接抛 IllegalStateException 拒绝启动。 - 2
"DEFAULT 0"确保在迁移过程中,已存在的记录会获得默认值0. - 3、Android Sqlite其实不支持删除列、修改列等操作,具体情况请参照下表
| 操作 | SQL 关键写法 | 是否要重建表 |
|---|---|---|
| 新增可空列 | ADD COLUMN xxx TYPE | 否 |
| 新增非空列 | ADD COLUMN xxx TYPE NOT NULL DEFAULT xxx | 否(最常用) |
| 新增唯一索引 | CREATE UNIQUE INDEX index_xxx ON table(col) | 否 |
| 删除列 / 改列属性 | 必须重建表(CREATE → INSERT → DROP → RENAME) | 是 |
| 改表名 | 必须重建表 | 是 |
- 4、我的小建议:
尽量不要修改删除已有的列,可新增列,废弃原有的列即可,降低升级风险。
四、Room + Flow:复杂关系型数据的最佳实践
1. Repository 层统一暴露Flow(结合网络获取+ 本地加载)
kotlin
/**
* 封装数据源,可使用数据库及网络数据源
*
* @param api 网络API服务接口
* @param userDao 本地数据库DAO接口
*/
class UserRepository(private val api: ApiService, private val userDao: UserDao) {
/**
* 用户数据流,结合网络和本地数据源
* 首先加载本地数据,然后请求网络数据更新本地数据库,最后监听数据库变化实时更新
* 暴露给Viewmodel的统一数据源
*/
val userFlow: Flow<Resource<List<UserEntity>>> = flow {
emit(Resource.Loading())
val localData = userDao.queryAll()
if (localData.isNotEmpty()) {
emit(Resource.Success(localData))
}
// 请求网络数据并同步到本地数据库
try {
val netData = api.getUsers()
val entity = netData.map {
UserEntity(
id = if (it.id > 0) it.id.toLong() else 0, // 0 时让数据库自动生成
username = it.username,
phoneNum = it.phone,
email = it.email
)
}
userDao.insertAll(entity)
} catch (e: Exception) {
val cached = userDao.queryAll()
emit(Resource.Error(e.message ?: "网络请求失败", data = cached))
}
// 监听数据库变化,自动刷新UI
emitAll(
userDao.observerAll()
.map<List<UserEntity>, Resource<List<UserEntity>>> { list -> Resource.Success(list) }
.catch { t -> emit(Resource.Error(t.message ?: "数据库读取失败")) }
)
}
.flowOn(Dispatchers.IO)
/**
* 添加用户到数据库
*
* @param userEntity 要添加的用户实体
* @return 插入记录的ID
*/
suspend fun addUser(userEntity: UserEntity): Long = userDao.insert(userEntity)
/**
* 更新数据库中的用户信息
*
* @param userEntity 包含更新信息的用户实体
*/
suspend fun updateUser(userEntity: UserEntity) = userDao.update(userEntity)
/**
* 根据邮箱删除用户
*
* @param email 要删除用户的邮箱地址
*/
suspend fun deleteUser(email: String) = userDao.deleteByEmail(email)
/**
* 根据用户名查询用户
*
* @param userName 要查询的用户名
* @return 查询到的用户实体,如果不存在则返回null
*/
suspend fun getUserByEmail(userName: String): UserEntity? = userDao.queryByUsername(userName)
/**
* 删除所有用户数据
*/
suspend fun deleteAll() = userDao.deleteAll()
}
2.封装Resource状态(非必须,但建议)
kotlin
/**
* 密封类 Resource 用于表示数据加载状态的通用包装类
*
*/
sealed class Resource<T>(
val data: T? = null,
val message: String? = null
) {
/**
* Loading 子类表示数据正在加载状态
*
* @param T 泛型参数,表示包装的数据类型
* @param data 加载过程中可能存在的旧数据,可为空
*/
class Loading<T>(data: T? = null) : Resource<T>(data)
/**
* Success 子类表示数据加载成功状态
*
* @param T 泛型参数,表示包装的数据类型
* @param data 成功加载的数据,不可为空
*/
class Success<T>(data: T?) : Resource<T>(data)
/**
* Error 子类表示数据加载失败状态
*
* @param T 泛型参数,表示包装的数据类型
* @param message 错误信息,不可为空
* @param data 加载失败时可能存在的部分数据,可为空
*/
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}
3.Viewmodel调用
kotlin
/**
* RoomViewModel类用于管理用户数据的UI状态和业务逻辑
* @param repository 用户数据仓库,用于执行数据操作
*/
class RoomViewModel(private val repository: UserRepository): ViewModel() {
/**
* 用户UI状态的流,将仓库中的用户数据流转换为UI状态流
* 根据数据加载状态显示不同的UI状态(加载中、成功、错误)
*/
val users: StateFlow<UserUiState> = repository.userFlow.map { resource ->
when (resource) {
is Resource.Success -> UserUiState(userList = resource.data)
is Resource.Error -> UserUiState(userList = resource.data, error = resource.message)
is Resource.Loading -> UserUiState(isLoading = true, userList = resource.data)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UserUiState(isLoading = true)
)
/**
* 刷新用户数据
* 重新从仓库获取最新的用户数据流
*/
fun refresh() {
viewModelScope.launch {
repository.userFlow.first()
}
}
/**
* 添加新用户
* @param userEntity 要添加的用户实体对象
*/
fun addUser(userEntity: UserEntity) {
viewModelScope.launch {
repository.addUser(userEntity)
}
}
/**
* 删除指定邮箱的用户
* @param email 要删除用户的邮箱地址
*/
fun deleteUser(email: String) {
viewModelScope.launch {
repository.deleteUser( email)
}
}
/**
* 清空所有用户数据
*/
fun clearUser() {
viewModelScope.launch {
repository.deleteAll()
}
}
}
/**
* 用户界面状态数据类
* @param isLoading 是否正在加载数据
* @param userList 用户列表数据
* @param error 错误信息
*/
data class UserUiState(
val isLoading: Boolean = false,
val userList: List<UserEntity>? = null,
val error: String? = null
)
相关知识点讲解:
repository.userFlow是用户数据流,由 UserRepository发射。其中map将上游数据流(Resource 类型的对象)转化成我们UI状态流(UserUiState)。使用stateIn将其转换为StateFlow以便在ViewModel中使用。UI界面通过接收UI状态流自动刷新界面。
stateIn 三个参数的使用说明:
-
- scope = viewModelScope
作用域参数:指定状态流的生命周期范围
功能:当 viewModelScope 取消时,状态流也会被取消
意义:确保 users 状态流与 RoomViewModel 的生命周期绑定,避免内存泄漏
- scope = viewModelScope
-
- started = SharingStarted.WhileSubscribed(5000)
启动策略参数:控制流的共享和订阅行为
功能:当有订阅者时开始收集数据,无订阅者时延迟5000毫秒停止收集
意义:优化性能,在无UI订阅时暂停数据收集,有新订阅时重新开始
- started = SharingStarted.WhileSubscribed(5000)
-
- initialValue = UserUiState(isLoading = true)
初始值参数:设置状态流的初始状态
功能:流创建时立即发射的默认值
意义:确保UI在首次订阅时能立即获得加载状态,提供良好的用户体验 这三个参数共同确保了状态流的生命周期管理、性能优化和用户体验。
- initialValue = UserUiState(isLoading = true)
4.Activity调用
kotlin
private fun observeData() {
lifecycleScope.launch {
viewModel.users.collect { state ->
when {
state.isLoading && state.userList == null -> {
//加载中
}
else -> {
//设置Recycleview数据源
state.userList?.let { adapter.setData(it) }
state.error?.let {
Toast.makeText(this@RoomActivity, "网络错误", Toast.LENGTH_LONG).show()
}
}
}
}
}
}
5.Room 自动更新的 6 个关键点(必须全部命中才触发)
| 关键点 | 具体实现 | 缺一不可的原因 |
|---|---|---|
| 1. 返回 Flow / LiveData / Flowable | 只有这三种返回值 Room 才会开启自动刷新机制 | 普通 suspend 函数不会监听 |
| 2. @Query(不能是 @Insert/@Update/@Delete) | 只有 @Query 会被 Room 解析出它监听了哪些表 | Room 要知道你关心哪些表 |
| 3. 数据库真的发生变更 | INSERT、UPDATE、DELETE、REPLACE、事务批量操作等 | 只读查询不会触发 |
| 4. 变更的表在 @Query 里出现过 | Room 会解析你的 SQL,提取 FROM/JOIN/UPDATE 等涉及的表名 | 表没出现在 SQL 里,Room 认为你不关心 |
| 5. 事务已经提交(commit) | 触发器只在事务真正提交后才执行 | 未提交的事务不算变更 |
| 6. 同一个 Database 实例 | 多进程下需要启用 multi-instance invalidation(Android 9+)才行 | 不同进程默认互不感知 |
总结:Room 自动更新的核心一句话
Room 在建库时默默地为每一个表创建了 INSERT/UPDATE/DELETE 触发器, 触发器通知 InvalidationTracker → 失效哪些 @Query → 重新执行并发射到 Flow/LiveData。
这套机制比 DataStore 的 FileObserver 更精准、更高效(毫秒级、无轮询),也是为什么普遍认为"Room + Flow" 是目前 Android 最优雅的本地数据库自动刷新方案。
四、Room和Proto DataStore的选择
Google 官方给出的"红线"建议:
| 数据类型 | 推荐存储方式 | 原因 |
|---|---|---|
| 简单配置、开关、Token、主题等 | DataStore(首选) | 轻量、类型安全、协程支持 |
| 任何 List、复杂对象 > 50 条 | Room | 支持分页、局部更新、索引 |
| 用户信息、设置项(单对象) | DataStore(可以) | 但建议字段不要超过 30 个 |
| 任何需要分页加载的数据 | Room + Paging 3 | DataStore 完全无法实现 |
| 离线缓存、商品列表、订单记录 | Room | 必选 |
什么时候真的可以用 Proto DataStore 存"稍复杂对象"?
可以,但必须同时满足以下全部条件:
- 对象足够小(强烈建议 < 50KB)
- 更新频率低 < 10 次/分钟
- 永远不会变成列表(哪怕现在只有一个字段,未来可能扩也别用)
- 不需要对内部字段做查询、分页、排序
满足以上才可以用,否则立刻改用 Room。
总结一句话:
DataStore 是一个"配置中心",不是"数据库"。 把它当数据库用,就相当于用 SharedPreferences 存 1000 条 JSON 一样(能跑,但一定卡的你怀疑人生)。
其他相关的界面不再列出,如需要请下载源码查看。