Android Jetpack 存储篇(DataStore、Room)与 Flow 高效组合

前言:为什么要写这篇文章

在现代 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...

graph TD A[UI] --> B[ViewModel StateFlow] B --> C[Repository Flow] C --> D[Room Flow] & E[DataStore.data Flow] D & E --> F[自动刷新]

本文主要讲述两者的使用,以及和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 三个参数的使用说明:

    1. scope = viewModelScope
      作用域参数:指定状态流的生命周期范围
      功能:当 viewModelScope 取消时,状态流也会被取消
      意义:确保 users 状态流与 RoomViewModel 的生命周期绑定,避免内存泄漏
    1. started = SharingStarted.WhileSubscribed(5000)
      启动策略参数:控制流的共享和订阅行为
      功能:当有订阅者时开始收集数据,无订阅者时延迟5000毫秒停止收集
      意义:优化性能,在无UI订阅时暂停数据收集,有新订阅时重新开始
    1. initialValue = UserUiState(isLoading = true)
      初始值参数:设置状态流的初始状态
      功能:流创建时立即发射的默认值
      意义:确保UI在首次订阅时能立即获得加载状态,提供良好的用户体验 这三个参数共同确保了状态流的生命周期管理、性能优化和用户体验。

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 存"稍复杂对象"?

可以,但必须同时满足以下全部条件:

  1. 对象足够小(强烈建议 < 50KB)
  2. 更新频率低 < 10 次/分钟
  3. 永远不会变成列表(哪怕现在只有一个字段,未来可能扩也别用)
  4. 不需要对内部字段做查询、分页、排序

满足以上才可以用,否则立刻改用 Room。

总结一句话:

DataStore 是一个"配置中心",不是"数据库"。 把它当数据库用,就相当于用 SharedPreferences 存 1000 条 JSON 一样(能跑,但一定卡的你怀疑人生)。

其他相关的界面不再列出,如需要请下载源码查看。

相关推荐
W.Y.B.G42 分钟前
vue3项目中集成天地图使用示例
android·前端
Haha_bj42 分钟前
二、Kotlin数组(Array)
android·app
t***265942 分钟前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb
y***13641 小时前
【MySQL】MVCC详解, 图文并茂简单易懂
android·数据库·mysql
w***48821 小时前
【MySQL】视图、用户和权限管理
android·网络·mysql
阿道夫小狮子1 小时前
Android 反射
android·前端·javascript
沐怡旸1 小时前
【翻译】scrcpy(3.3.3)命令使用文档
android
沐怡旸1 小时前
【翻译】adb(Android Debug Bridge) 帮助文档
android
QING6181 小时前
Kotlin 协程中Job和SupervisorJob —— 新手指南
android·kotlin·android jetpack