21、本地数据存储:Room数据库集成、DataStore(Preferences与Proto)、SharedPreferences迁移
本地数据存储,说白了就是让你的App在没网的时候也能干活。用户关掉App再打开,数据还在,这才是正经的移动应用。
我个人习惯把本地存储分成两类:结构化数据和键值对数据。前者用Room,后者用DataStore。至于SharedPreferences?嗯,它已经过时了,但很多老项目还在用,所以迁移方案也得讲清楚。
21.1 Room数据库:结构化数据的首选
Room是Google官方推出的ORM框架,底层封装了SQLite。我刚开始用的时候觉得它有点重,但用久了发现——真香。编译时检查SQL语句,省了多少运行时崩溃啊。
21.1.1 添加依赖
// build.gradle.kts (Module)
plugins {
id("kotlin-kapt") // 或者 id("com.google.devtools.ksp")
}
dependencies {
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
}
21.1.2 定义实体和DAO
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int,
val email: String?
)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
注意看,getAllUsers()返回的是Flow。这意味着只要数据库里的users表有变化,UI会自动更新。我在项目中就用这个特性做了实时搜索,用户体验非常好。
21.1.3 创建数据库
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"my_app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
21.2 DataStore:SharedPreferences的现代替代品
DataStore有两种口味:Preferences DataStore和Proto DataStore。前者存键值对,后者存类型化对象。我个人更推荐Proto DataStore,因为它类型安全,而且支持数据迁移。
21.2.1 Preferences DataStore
// 创建
val Context.dataStore by preferencesDataStore(name = "settings")
// 读取
val themeMode: Flow<String> = context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.THEME_KEY] ?: "light"
}
// 写入
suspend fun setThemeMode(mode: String) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.THEME_KEY] = mode
}
}
private object PreferencesKeys {
val THEME_KEY = stringPreferencesKey("theme_mode")
}
你看,DataStore天然支持Flow,这意味着你可以直接在Compose里用collectAsState()。SharedPreferences做不到这一点,你得手动注册监听器,麻烦得很。
21.2.2 Proto DataStore
Proto DataStore需要先定义protobuf文件。别怕,其实很简单。
// user_preferences.proto
syntax = "proto3";
option java_package = "com.example.app";
option java_multiple_files = true;
message UserPreferences {
string theme_mode = 1;
bool notifications_enabled = 2;
int32 font_size = 3;
}
然后在build.gradle里配置protobuf插件,生成Java类。接着创建DataStore:
val Context.userPreferencesStore by dataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer
)
class 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)
}
}
21.3 SharedPreferences迁移
老项目肯定有SharedPreferences。直接删掉?不行,用户数据会丢。我做过一次迁移,踩了不少坑,这里把经验分享给你。
21.3.1 迁移到DataStore
DataStore官方提供了迁移API,可以直接从SharedPreferences迁移过来。
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context,
"old_shared_prefs_name"
)
)
}
)
就这么简单?嗯,基本是这样。但要注意:迁移是一次性的。一旦迁移完成,SharedPreferences里的数据就不会再被读取了。所以迁移前一定要做好备份。
21.3.2 迁移到Room
如果SharedPreferences里存的是结构化数据(比如用户信息列表),那应该迁移到Room。我一般写一个迁移脚本:
class MigrationTask(private val context: Context) {
suspend fun migrate() {
val prefs = context.getSharedPreferences("user_data", Context.MODE_PRIVATE)
val json = prefs.getString("users", null) ?: return
val gson = Gson()
val users: List<User> = gson.fromJson(json, object : TypeToken<List<User>>() {}.type)
val db = AppDatabase.getInstance(context)
users.forEach { user ->
db.userDao().insertUser(user)
}
// 迁移完成后删除旧数据
prefs.edit().remove("users").apply()
}
}
synchronized或者Mutex保证迁移只执行一次。
21.4 知识体系总览
下面这张图帮你理清本地数据存储的选型逻辑:
21.5 实战建议
说了这么多,到底怎么选?我给你一个简单的判断标准:
- 数据有关系吗? 比如用户和订单是一对多关系 → 用Room
- 数据只是简单配置? 比如主题、语言、开关 → 用DataStore
- 需要类型安全? 用Proto DataStore
- 老项目有SharedPreferences? 迁移到DataStore,别犹豫
你想想看,用户的数据安全是第一位的。选对了存储方案,后面维护起来省心得多。我见过太多项目因为一开始图省事用了SharedPreferences存复杂数据,结果后面改得痛不欲生。
好了,本地数据存储就讲到这里。记住:选型比实现更重要。花10分钟想清楚用哪个,能省你后面10小时的改bug时间。
公众号:蓝海资料掘金营,微信deep3321