Android开发拾遗:如何使用Proto DataStore
持久化一些用户数据是一个常见的需求,例如保存用户设置,Android提供了一个方便的机制,叫做DataStore。其中有两套API,一个是Preferences DataStore
,可以存取简单的key-value
数据;另一个是Proto DataStore
,顾名思义,这需要开发者定义一个protocol buffers的schema,可以存取自定义的数据类型,并提供类型安全保证。
最初我仅用到了简单的键值对API,因为业务上需要的配置项逐渐变多,并且在一些地方我需要使用枚举,所以萌生了从Preferences DataStore
转到Proto DataStore
的念头。但可惜的是,Android的文档关于Proto DataStore
没有详细的描述,在参考了一些开源代码后,我找到了能适配Kotlin
以及Gradle Kotlin DSL
的使用方法,在此记录一下。
Schema
首先要在app/src/main/proto
目录下创建一个protobuf文件,如settings.proto
:
syntax = "proto3";
// 这里替换成自己的包名
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Gradle配置
有了schema之后,还需要有对应的用来序列化/反序列化的数据结构,这个数据结构可以通过库来生成。修改Gradle配置:
import com.google.protobuf.gradle.id
plugins {
// ...
id("com.google.protobuf") version "0.9.1"
}
dependencies {
// 添加这两个依赖
implementation("androidx.datastore:datastore:1.1.1")
implementation("com.google.protobuf:protobuf-javalite:3.17.3")
}
// 这里Android Studio可能有lint报错,直接忽略,sync gradle
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.21.9"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
id("java") {
option("lite")
}
}
}
}
}
在代码中使用
先创建一个SettingsSerializer.kt
文件,定义序列化器:
// 这个Settings类是自动生成的
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)
}
val Context.settingsDataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)
读取时就可以使用冷流:
val settings = context.settingsDataStore.data.first()
如果要设置值,可以使用updateData
方法:
suspend fun incrementCounter() {
context.settingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setExampleCounter(currentSettings.exampleCounter + 1)
.build()
}
}