DataStore
简介
Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。
DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据,克服了传统 SharedPreferences 的一些缺点
如果您目前是使用 SharedPreferences 存储数据的,请考虑迁移到 DataStore。
优点
类型安全:DataStore使用Kotlin协程和Flow,提供类型安全的API,避免了传统SharedPreferences中的类型错误。
异步操作:所有的读写操作都是异步的,避免了主线程阻塞,提升了应用的响应性。
更好的性能:DataStore基于Proto DataStore或Preferences DataStore,提供了更高的性能和更少的内存占用。
支持流式数据:可以使用Flow来观察数据变化,方便实现实时更新。
易于扩展:DataStore更易于扩展,支持更复杂的数据结构,比如使用ProtoBuf。
依赖设置
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。
Preferences DataStore:使用较为简单。使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
Proto DataStore:使用较为复杂。将数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。
如需在您的应用中使用 Jetpack DataStore,请根据您要使用的实现向 Gradle 文件添加以下内容:
Preferences DataStore
[versions]
datastore = "1.1.1"
[libraries]
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
# 可选:不带 Android 依赖的核心库
androidx-datastore-preferences-core = { group = "androidx.datastore", name = "datastore-preferences-core", version.ref = "datastore" }dependencies {
implementation(libs.androidx.datastore.preferences)
}Proto DataStore
[libraries]
androidx-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }dependencies {
implementation(libs.androidx.datastore)
}正确使用 DataStore
为了正确使用 DataStore,请始终谨记以下规则:
请勿在同一进程中为给定文件创建多个 DataStore 实例,否则会破坏所有 DataStore 功能。 如果给定文件在同一进程中有多个有效的 DataStore 实例,DataStore 在读取或更新数据时将抛出 IllegalStateException。
DataStore 的通用类型必须不可变。更改 DataStore 中使用的类型会导致 DataStore 提供的所有保证都失效,并且可能会造成严重的、难以发现的 bug。 强烈建议您使用 协议缓冲区,这些缓冲区提供保证不变性、简单的 API 以及 高效序列化。
切勿对同一个文件混用 SingleProcessDataStore 和 MultiProcessDataStore。 如果您想要从多个位置访问 DataStore,请执行以下操作: 进程,始终使用 MultiProcessDataStore。
Preferences DataStore
Preferences DataStore 实现使用 DataStore 和 Preferences 类将简单的键值对保留在磁盘上。
创建DataStore
您可以使用 preferencesDataStore 所创建的属性委托来创建 Datastore<Preferences> 实例。 在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。 这样可以更轻松地将 DataStore 保留为单例。 如果您使用的是 RxJava,请使用 RxPreferenceDataStoreBuilder。 必需的 name 参数是 Preferences DataStore 的名称。
在项目顶层文件中添加这样一个扩展
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")读取内容
由于 Preferences DataStore 不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。 例如,如需为 int 值定义一个键,请使用 intPreferencesKey()。 然后,使用 DataStore.data 属性,通过 Flow 提供适当的存储值
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val counterFlow: Flow<Int> = context.dataStore.data.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}目前提供了如下几种类型的PreferencesKey:
//androidx.datastore.preferences.core.PreferencesKeys
intPreferencesKey()
doublePreferencesKey()
stringPreferencesKey()
booleanPreferencesKey()
floatPreferencesKey()
longPreferencesKey()
stringSetPreferencesKey()
byteArrayPreferencesKey()写入内容
Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。 该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}扩展
为了进一步简化数据的读写操作,可以根据需要定义几个扩展函数,类似这样:
fun <T> DataStore<Preferences>.readValue(key: Preferences.Key<T>): Flow<T?> = data.catch {
//可以根据需求处理读取出错时的逻辑,这里是返回空
emit(emptyPreferences())
}.map {
it[key]
}
suspend fun <T> DataStore<Preferences>.writeValue(
key: Preferences.Key<T>, value: T
): Result<Preferences> = runCatching {
edit {
it[key] = value
}
}这样数据的读写操作就可以替换成这样了
context.dataStore.readValue(EXAMPLE_COUNTER).firstOrNull() ?: 0
context.dataStore.writeValue(EXAMPLE_COUNTER, 1)Proto DataStore
Proto DataStore 实现使用 DataStore 和协议缓冲区将类型化对象保留在磁盘上。
定义架构
Proto DataStore 要求在 app/src/main/proto/ 目录下的 proto 文件中保存预定义的架构。 此架构用于定义您在 Proto DataStore 中保存的对象的类型。 如需详细了解如何定义 proto 架构,请参阅 protobuf 语言指南。
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}创建DataStore
创建 Proto DataStore 来存储类型化对象涉及两个步骤:
定义一个实现
Serializer<T>的类,其中T是proto文件中定义的类型。此序列化器类会告知DataStore如何读取和写入您的数据类型。 请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。使用
dataStore所创建的属性委托来创建DataStore<T>实例,其中T是在proto文件中定义的类型。 在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename参数会告知DataStore使用哪个文件存储数据,而serializer参数会告知DataStore在第 1 步中定义的序列化器类的名称。
//Step1
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Settings {
try {
return Settings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(
t: Settings,
output: OutputStream
) = t.writeTo(output)
}
//Step2
val Context.settingsDataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)读取内容
使用 DataStore.data 显示所存储对象中相应属性的 Flow。
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data.map { settings ->
// The exampleCounter property is generated from the proto schema.
settings.exampleCounter
}写入内容
Proto DataStore 提供了一个 updateData() 函数,用于以事务方式更新存储的对象。 updateData() 为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。
suspend fun incrementCounter() {
context.settingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setExampleCounter(currentSettings.exampleCounter + 1)
.build()
}
}在同步代码中使用 DataStore
⚠️ 注意
请尽可能避免在进行 DataStore 数据读取时阻塞线程。阻塞界面线程可能会导致 ANR 或界面卡顿,而阻塞其他线程可能会导致死锁。
DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。 如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,可能就会如此。
Kotlin 协程提供 runBlocking() 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 runBlocking() 从 DataStore 同步读取数据。 RxJava 提供了针对 Flowable 的阻塞方法。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据:
val exampleData = runBlocking { context.dataStore.data.first() }对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题:
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
context.dataStore.data.first()
// You should also handle IOExceptions here.
}
}这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 runBlocking() 进行同步读取的速度可能会更快,如果初始读取操作已经完成,或许还可以完全避免磁盘 I/O 操作。
在多进程代码中使用 DataStore
⭐ 注意
DataStore 多进程功能目前在 1.1.0 版中提供
您可以对 DataStore 进行配置,使其在不同进程中访问相同数据时确保实现与在单个进程中访问数据时相同的数据一致性。具体而言,DataStore 可保证:
- 读取仅返回已持久存储到磁盘的数据。
- 写后读一致性。
- 写入会序列化。
- 写入绝不会阻塞读取。
为了能够在不同进程中使用 DataStore,您需要使用 MultiProcessDataStoreFactory 构造 DataStore 对象。 以下是一个创建 MultiProcessDataStoreFactory 的简单示例,读写方法和之前一样。
您需要根据自己的项目结构来确保 DataStore 实例在每个进程中具有唯一性:
private val dataStoreFile: File by lazy {
preferencesDataStoreFile("settings")
}
//Proto DataStore
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
dataStoreFile
}
)官方的文档中貌似只有这一个创建 Proto DataStore 的例子,但是项目中使用的不是这个,而是 Preferences DataStore, 又需要多进程使用的话应该怎么办。
看了下 MultiProcessDataStoreFactory 的源码,它有两个创建函数:
public fun <T> create(
storage: Storage<T>,
corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
migrations: List<DataMigration<T>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<T> = DataStoreImpl<T>(
storage = storage,
initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
scope = scope
)
public fun <T> create(
serializer: Serializer<T>,
corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
migrations: List<DataMigration<T>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile: () -> File
): DataStore<T> = DataStoreImpl<T>(
storage = FileStorage(
serializer,
{ MultiProcessCoordinator(scope.coroutineContext, it) },
produceFile
),
initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
scope = scope
)除了上述例子中使用的创建方法,还有一个方法中可以传 Storage<T>,而官方提供了两种存储接口,可切换 Datastore 的底层存储机制。 分别是 java.io(对应 class FileStorage)和 okio(对应 class OkioStorage)的实现。
默认的 PreferenceDataStoreFactory 创建使用的便是 OkioStorage:
//PreferenceDataStoreFactory
@JvmOverloads
public fun create(
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
migrations: List<DataMigration<Preferences>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile: () -> File
): DataStore<Preferences> {
val delegate = create(
storage = OkioStorage(FileSystem.SYSTEM, PreferencesSerializer) {
val file = produceFile()
check(file.extension == PreferencesSerializer.fileExtension) {
"File extension for file: $file does not match required extension for" +
" Preferences file: ${PreferencesSerializer.fileExtension}"
}
file.absoluteFile.toOkioPath()
},
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
)
return PreferenceDataStore(delegate)
}
//OkioStorage
public class OkioStorage<T>(
private val fileSystem: FileSystem,
private val serializer: OkioSerializer<T>,
private val coordinatorProducer: (Path, FileSystem) -> InterProcessCoordinator = { path, _ ->
createSingleProcessCoordinator(path)
},
private val producePath: () -> Path
) : Storage<T>可以看到 OkioStorage 的默认构造函数中生产者使用的是createSingleProcessCoordinator,也就是单进程的。 那我们使用 OkioStorage 自己构造 Storage<T> 并传入多进程的生产者是不是就行了
就像这样:
import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.createMultiProcessCoordinator
import androidx.datastore.core.okio.OkioStorage
import androidx.datastore.preferences.core.PreferencesSerializer
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import java.io.File
//...
private val dataStoreFile: File by lazy {
preferencesDataStoreFile("settings")
}
val dataStore: DataStore<Preferences> by lazy {
MultiProcessDataStoreFactory.create(
storage = OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = PreferencesSerializer,
producePath = {
dataStoreFile.toOkioPath()
},
coordinatorProducer = { path, _ ->
createMultiProcessCoordinator(context = Dispatchers.IO, file = path.toFile())
}
)
)
}经过测试,这样写之后确实可以在多进程中使用,分别在两个进程中修改,双方都能同步变更:
MainActivity I write 456
MainActivity I data: 456
---- PROCESS STARTED (19039) for package com.example
MultiProcessActivity I data: 456
MultiProcessActivity I write 123
MultiProcessActivity I data: 123
MainActivity I data: 123在跨平台项目中使用 DataStore
以上例子均是在安卓项目中的写法,如果在别的平台上,比如 ComposeDesktop 中也可以使用 DataStore。
首先,需要依赖库 datastore-preferences-core,而不是 datastore-preferences :
dependencies {
implementation("androidx.datastore:datastore-preferences:1.1.1")
implementation("androidx.datastore:datastore-preferences-core:1.1.1")
}然后,使用 PreferenceDataStoreFactory 创建 DataStore:
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
private val dataStore: DataStore<Preferences> = PreferenceDataStoreFactory.create(produceFile = {
File($fileDir, "${fileName}.preferences_pb")
})其他使用方法和之前一样。
⭐ 注意
存储文件的后缀必须是 .preferences_pb
迁移到DataStore
如果项目中之前使用的是传统的 SharedPreferences,现在想换成 DataStore,但是不想丢失之前的配置,可以进行迁移。
在创建 DataStore 时,添加 produceMigrations 将原有的 SharedPreferences 迁移过来:
private val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, sharedPreferencesName = "settings"))
}
)之后将原来的key,换成 DataStore 的 PreferencesKey(),读写的地方都相应换成 DataStore 的读写方法。