第3章 创建 Preferences DataStore 实例:属性委托与单例模式
3.1 原理概述
Android 官方推荐的 Preferences DataStore 创建方式基于 属性委托(Property Delegation),通过 Kotlin 的 by 关键字将 DataStore<Preferences> 实例的创建与生命周期绑定到 Context 的扩展属性上。这种方式既简化了样板代码,又保证了实例的惰性初始化和内存安全性。
同时,为避免应用中多个组件重复创建 DataStore 实例导致文件竞争和资源浪费,必须采用 单例模式 设计。结合属性委托,通常将 DataStore 实例声明为顶层扩展属性,或置于 companion object 内,并通过 by lazy 确保全局唯一性。
关键的构造参数 context 和 fileName 决定了两件事:
- context:必须使用
Application Context(而非 Activity/Service 上下文),防止因组件销毁导致 DataStore 持有过期引用。 - fileName:偏好设置文件在内部存储中的名称(不含路径),需保持唯一且可读,建议使用小写字母和下划线,如
user_preferences。
3.2 实现流程
- 在模块级
build.gradle.kts中添加 DataStore 依赖(假设已添加,见第2章)。 - 定义顶层扩展属性或单例对象,使用
by preferencesDataStore(name = "...")委托。 - 将
context参数传入PreferenceDataStoreFactory.create或直接使用预置委托函数(内部已处理上下文)。 - 在 Activity / Application / ViewModel 中通过
context.dataStore访问单例实例。
3.3 代码示例:属性委托与单例模式
// ========== 方案一:顶层扩展属性 + by lazy 实现单例 ==========
// 直接使用官方 preferencesDataStore 委托(内部使用 by lazy)
private val Application.dataStore by preferencesDataStore(name = "user_preferences")
// 若需自定义 factory,可替换为:
val Application.dataStore: DataStore<Preferences> by lazy {
PreferenceDataStoreFactory.create {
applicationContext.preferencesDataStoreFile("user_preferences")
}
}
// ========== 方案二:单例对象托管 ==========
object DataStoreProvider {
private val appContext: Context by lazy {
// 建议通过 ContentProvider 或 Application.attachBaseContext 获取全局 Context
// 这里仅演示,实际应注入
throw NotImplementedError("Inject Application context properly")
}
val dataStore: DataStore<Preferences> by lazy {
PreferenceDataStoreFactory.create {
appContext.preferencesDataStoreFile("user_preferences")
}
}
}
// ========== 推荐写法(官方标准):直接在 Application 类中定义 ==========
class MyApp : Application() {
companion object {
val dataStore: DataStore<Preferences> by lazy {
PreferenceDataStoreFactory.create {
applicationContext().preferencesDataStoreFile("user_preferences")
}
}
// 用于获取 Application Context 的便捷方法
private lateinit var instance: MyApp
fun applicationContext(): MyApp = instance
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
// ========== 在 Activity 中直接通过属性委托访问 ==========
class MainActivity : AppCompatActivity() {
// 方式一:持有 Application 级别的 DataStore
private val dataStore = MyApp.dataStore
// 方式二(如果采用顶层扩展属性):
// private val dataStore by application.dataStore
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 数据读写操作见后续章节
}
}
3.4 参数说明与对比
| 参数 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
context | Context | 必须为 Application Context,避免内存泄漏 | applicationContext 或 MyApp.instance |
fileName | String | 内部存储的文件名,不含路径,应全局唯一 | "user_preferences" |
produceFile(工厂参数) | (Context) -> File | 自定义文件路径,默认使用 preferencesDataStoreFile | 通常保持默认 |
3.5 调试与验证
在 Application 的 onCreate 中打印 DataStore 实例的哈希码,确认单例唯一性:
// 在 MyApp.onCreate() 中
Log.d("DataStore", "DataStore instance: ${MyApp.dataStore} hash: ${System.identityHashCode(MyApp.dataStore)}")
// 在其他 Activity 中重复调用,应打印相同哈希值
也可通过 Android Studio 的 Device File Explorer 查看 /data/data/your.package.name/files/datastore/ 目录下是否生成了 user_preferences.preferences_pb 文件。
3.6 常见错误与避坑指南
- ❌ 使用 Activity Context 创建 DataStore:Activity 销毁后 DataStore 可能持有悬空引用,导致崩溃或数据丢失。
✅ 始终使用applicationContext或 Application 实例。 - ❌ 每次读写都新建 DataStore 实例:违反单例原则,引发文件锁竞争。
✅ 通过属性委托 +by lazy自动保证全局唯一。 - ❌ fileName 包含路径或特殊字符:
preferencesDataStoreFile会自动拼接路径,传入路径会导致异常。
✅ 仅使用字母数字和下划线,如"app_settings"。 - ❌ 在非 UI 线程创建 DataStore:
PreferenceDataStoreFactory.create内部使用Dispatchers.IO,但若手动初始化在 UI 线程可能增加启动耗时。
✅ 惰性初始化自然延后到首次访问,无需刻意处理。
3.7 性能与稳定性优化
- 使用 Application 级别的单例:避免因 Activity 重建导致 DataStore 重新创建,减少 I/O 开销。
- 避免频繁调用
preferencesDataStoreFile:该函数每次调用都会检查目录存在性,建议只在惰性初始化时执行。 - 合理设置文件名:若应用有多个独立偏好设置模块,可创建多个 DataStore(如
"user_prefs"和"feature_flags"),但每个 DataStore 文件应独立操作,减少锁争用。
💡 实践建议:将 DataStore 定义在
Application 类的 companion object 中,并暴露一个顶层扩展属性供组件直接调用。例如:// 在 Application 中
val Context.appDataStore: DataStore<Preferences> by preferencesDataStore(name = "app_settings")
3.8 本章核心流程图:DataStore 单例创建链路
3.9 本章小结
通过属性委托 by preferencesDataStore(name = "...") 配合 by lazy 机制,可以优雅地创建全局唯一的 DataStore 实例。关键点包括:
- 必须传入 Application Context(而非 Activity/Service 上下文)。
- 文件名使用小写字母和下划线,不含路径。
- 单例模式由 Kotlin 惰性初始化自然保证,无需手动加锁。
- 所有数据读写操作都通过同一个实例完成,避免并发文件操作问题。