Jetpack DataStore 是一种数据存储解决方案,主要适用于小型数据的处理。它可以通过协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。DataStore有两种实现方式(1)Preferences DataStore 和 (2)Proto DataStore。Proto DataStore将数据作为自定义类型的实例进行存储。
在了解Protocol Buffers之前需要了解序列化和反序列化。
序列化:就是将代码中生成的对象生成二进制流形式保存起来,也可以将二进制流写入到文件或其他媒介中存储或传输(如网络传输)。
反序列化:就是将二进制流转换成代码可以处理的对象。
Protocol Buffers翻译成协议缓存区,是Google公司推出的一种语言中立、平台中立的数据序列化的方法,类似xml,但是比xml更小、更加灵活,处理更快。
Protobuffer的工作流程:
首先需要通过在 .proto 文件中定义消息类型来指定数据的结构以及要序列化的服务;
接着根据proto文件可以在不同语言的代码中,将该文件自动生成对应的结构类型(消息的逻辑结构);根据结构类型创建对应的消息(要处理对象)。
然后,编译器使用协议对其进行编译。按照预先定义的架构用于对序列化数据进行编码和解码。解码生成序列化数据,以二进制数据的形式保存起来;也可以对可序列化数据进行编码,在代码中将这些二进制数据转换成代码需要的对象。
Proto Datastore采用Protocol Buffers来定义架构,使得按键存取值可以使用正确的数据类型。Proto DataStore 实现使用 DataStore 和协议缓冲区将类型化对象保留在磁盘上。仍然使用Android笔记(十九)类似的实例,通过示例来了解Proto DataStore的应用。
因为构建配置语言采用Kotlin DSL,因此配置的相同选项与谷歌官方网站介绍的Groovy DSL配置有细节不同。
添加协议缓存区插件
plugins{
…
id(“com.google.protobuf”) version “0.9.4”
}
添加协议缓存区和Proto DataStore依赖项
implementation(“androidx.datastore:datastore-core:1.0.0”)
implementation(“com.google.protobuf:protobuf-javalite:3.19.4”)
配置协议缓存区
在build.gradle.kt中增加如下protebuf的配置
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.23.4"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().forEach {task->
task.builtins {
id("java"){
option("lite")
}
}
}
}
}
另外,还需要指定转换文件的源目录,需要在build.gradle.kt中设置如下配置:
android{
...
sourceSets.named("main"){
proto{
srcDir("src/main/proto")
}
}
}
(1)安装Protocol Buffers插件
为了在Android Studio中更容易编辑.proto文件,可以在Android Studio中安装Protocol Buffers插件,如下图所示:
(2)创建.proto文件
在.proto文件中定义架构,采用上例的两个属性:账号名name和访问次数counter。
在app/src下创建一个新的目录,命名为proto,然后在新创建的proto目录下定义如下UserPreferences.proto文件:
UserPerferences.proto文件内容如下:
syntax = "proto3";
//java_package选项:java生成的类所在的包
option java_package = "com.eyes.datastore.utils";
//java_multiple_files选项:如果为 false,则只会.java为此文件生成一个.proto文件,如果true,.java将为每个 Java 类/枚举/等生成单独的文件。
option java_multiple_files = true;
message UserPreferences{
int32 counter = 1;//第一个关键字对应的取值类型为int32
string email = 2; //第二个关键字对应的取值类型是string
optional string name = 3; //可选项,第三个关键字对应的取值类型是string
}
有一个细节需要注意,.proto文件只能在project层次结构下可以查看到该文件。如果采用Android层次结构,则无法在目录层次结构中找到.proto文件。
(3)构建模块,自动生成对应Java代码
然后,构建该项目模块,可以发现在模块的project项目层次下的模块名-》build-》generated-》source-》proto-》java-》指定包名下会自动生成对应的java代码,如下图所示:
数据序列化分为两个步骤:
定义一个实现 Serializer 的类,其中 T 是 proto 文件中指定的类型。此序列化器类会告知 DataStore 如何读取和写入数据类型。
创建 DataStore 实例,其中 T 是在 proto 文件中定义的类型。在 Kotlin文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。
object UserPreferencesSerializer: Serializer<UserPreferences> {
override val defaultValue: UserPreferences
get() = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (e: InvalidProtocolBufferException) {
throw CorruptionException("无法读取UserPreferences.proto")
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
在文件的顶层创建Proto DataStore对象,代码如下:
val Context.userPreferencesDataStore: DataStore<UserPreferences> by dataStore(
fileName = "UserPreferences.pb",
serializer = UserPreferencesSerializer
)
上述的代码中:
filename 参数会告知 DataStore 使用哪个文件存储数据
serializer 参数会告知 DataStore 在第 1 步中定义的序列化器类的名称。
为了在Kotlin代码中更好地处理数据,这里定义一个实体类UserBean,使得可以将序列化的数据生成对应的UserBean对象。
data class UserBean(val counter:Int,val email:String,var name:String="客人")
这个实体类是可选项,可以根据项目的需求不需要定义。
定义DataStoreUtils实体类,实现对数据的具体的读写操作,代码如下:
class DataStoreUtils(val context: Context) {
val userFlow: Flow<UserBean> =
context.userPreferencesDataStore.data.map{userPreferences->
UserBean(userPreferences.counter,
userPreferences.email,
userPreferences.name)
}
suspend fun updateData(email:String,name:String="无昵称"){
context.userPreferencesDataStore.updateData {userPreferences->
userPreferences.toBuilder()
.setCounter(userPreferences.counter+1)
.setEmail(email)
.setName(name)
.build()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(dataStoreUtils: DataStoreUtils, modifier:Modifier = Modifier){
val context = LocalContext.current
var emailInput by remember{ mutableStateOf("guest@eyes.com") }
var nameInput by remember{ mutableStateOf("客人") }
var rememberInput by remember{ mutableStateOf(false) }
var content by remember{ mutableStateOf("") }
val scope = rememberCoroutineScope()
//读取数据
LaunchedEffect(Unit){
dataStoreUtils.userFlow.collect{userBean->
content = "账号:${userBean.email},昵称:${userBean.name}访问次数是:${userBean.counter}"
}
}
Column{
TextField(modifier = Modifier.fillMaxWidth(),
value = emailInput,
onValueChange = {it:String->
emailInput = it
},
label ={Text("输入账号")},
leadingIcon = {
Icon(imageVector = Icons.Filled.Email,contentDescription = "账号")
}
)
TextField(modifier = Modifier.fillMaxWidth(),
value = nameInput,
onValueChange = {it:String->
nameInput = it
},
label ={Text("输入昵称")},
leadingIcon = {
Icon(imageVector = Icons.Filled.Person,contentDescription = "昵称")
}
)
Row{
Checkbox(checked = rememberInput, onCheckedChange ={
rememberInput = it
if(rememberInput)
scope.launch {
//写入数据
dataStoreUtils.updateData(emailInput,nameInput)
}
})
Text("记住账号")
}
if(!emailInput.isBlank())
Text(text = content ,fontSize=20.sp)
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ForCourseTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainScreen(DataStoreUtils(this))
}
}
}
}
}
运行结果如下图所示
通过Device Explorer查看对应的项目模块的文件处理情况,在data->data->模块包名->files->datastore目录下的文件可序列化的数据存储在UserPerferences.pb中,具体文件内容如下图所示:
DataStore
https://developer.android.google.cn/topic/libraries/architecture/datastore?hl=zh-cn
Gradle Kotlin DSL Primer
https://docs.gradle.org/current/userguide/kotlin_dsl.html
customizing-protobuf-compilation
https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation