本地存储
Room/DataStore/文件。
本地存储
这一课你将能做到
- 根据场景选择正确的存储方案:DataStore(偏好)、Room(结构化)、文件(大文件)
- 使用 Room 完成数据库定义、DAO 操作、迁移策略
- 实现离线优先架构:网络数据缓存到本地,本地数据驱动 UI
为什么要学
移动网络不稳定,用户可能在地铁、电梯、偏远地区使用 App。本地存储决定了离线体验:是白屏转圈,还是流畅浏览缓存内容。Room 是 Google 官方的 SQLite 封装,DataStore 是 SharedPreferences 的现代替代。掌握它们,才能写出「有网没网都能用」的应用。
人话:本地存储就像给 App 装了一个「冰箱」。网络数据是外卖,到了就吃掉(展示);本地存储是冰箱存货,外卖送不到时也能吃上饭。DataStore 放「调料」(设置),Room 放「正餐」(结构化数据),文件系统放「大纸箱」(图片、视频)。
面试中,「Room 迁移」、「DataStore vs SharedPreferences」、「缓存策略」是高频考点。
先建立图像
┌─────────────────────────────────────────┐
│ 本地存储选型决策树 │
│ │
│ 数据类型? │
│ ├── 键值对(设置/Token) │
│ │ └── DataStore (首选) │
│ │ └── 替代 SharedPreferences
│ ├── 结构化数据(列表/关系) │
│ │ └── Room (SQLite) │
│ └── 大文件(图片/视频/日志) │
│ └── 内部/外部存储 │
│ │
│ 离线优先流程: │
│ 网络 → 存入 Room → UI 读取 Room → 显示 │
│ (网络是源头,但 UI 不直接依赖网络) │
└─────────────────────────────────────────┘
跟着做
步骤 1:DataStore 保存用户设置
// 定义 DataStore
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings"
)
class SettingsRepository @Inject constructor(
@ApplicationContext private val context: Context
) {
private object Keys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val FONT_SIZE = intPreferencesKey("font_size")
val LAST_SYNC = longPreferencesKey("last_sync")
}
// 读取:Flow 自动更新
val isDarkMode: Flow<Boolean> = context.dataStore.data
.map { it[Keys.DARK_MODE] ?: false }
val fontSize: Flow<Int> = context.dataStore.data
.map { it[Keys.FONT_SIZE] ?: 16 }
// 写入:挂起函数
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { it[Keys.DARK_MODE] = enabled }
}
suspend fun setFontSize(size: Int) {
context.dataStore.edit { it[Keys.FONT_SIZE] = size }
}
suspend fun updateLastSync() {
context.dataStore.edit {
it[Keys.LAST_SYNC] = System.currentTimeMillis()
}
}
}
步骤 2:Room 数据库定义
// 1. Entity
@Entity(tableName = "tracks")
data class TrackEntity(
@PrimaryKey val slug: String,
val title: String,
val description: String,
val lessonCount: Int,
val coverUrl: String?,
val updatedAt: Long = System.currentTimeMillis(),
val isFavorite: Boolean = false
)
@Entity(
tableName = "lessons",
foreignKeys = [
ForeignKey(
entity = TrackEntity::class,
parentColumns = ["slug"],
childColumns = ["trackSlug"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("trackSlug")]
)
data class LessonEntity(
@PrimaryKey val slug: String,
val trackSlug: String,
val title: String,
val content: String,
val order: Int,
val duration: Int
)
// 2. DAO
@Dao
interface TrackDao {
@Query("SELECT * FROM tracks ORDER BY updatedAt DESC")
fun getAllTracks(): Flow<List<TrackEntity>>
@Query("SELECT * FROM tracks WHERE slug = :slug")
suspend fun getTrackBySlug(slug: String): TrackEntity?
@Query("SELECT * FROM tracks WHERE isFavorite = 1")
fun getFavoriteTracks(): Flow<List<TrackEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTracks(tracks: List<TrackEntity>)
@Query("UPDATE tracks SET isFavorite = NOT isFavorite WHERE slug = :slug")
suspend fun toggleFavorite(slug: String)
@Delete
suspend fun deleteTrack(track: TrackEntity)
@Transaction
@Query("SELECT * FROM tracks WHERE slug = :slug")
suspend fun getTrackWithLessons(slug: String): TrackWithLessons?
}
// 关系查询数据类
data class TrackWithLessons(
@Embedded val track: TrackEntity,
@Relation(
parentColumn = "slug",
entityColumn = "trackSlug"
)
val lessons: List<LessonEntity>
)
// 3. Database
@Database(
entities = [TrackEntity::class, LessonEntity::class],
version = 2,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun trackDao(): TrackDao
abstract fun lessonDao(): LessonDao()
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,
"coderoadmap.db"
)
.addMigrations(MIGRATION_1_2)
.fallbackToDestructiveMigration() // 仅开发阶段使用
.build()
INSTANCE = instance
instance
}
}
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE tracks ADD COLUMN isFavorite INTEGER NOT NULL DEFAULT 0")
}
}
}
}