1. 前言
Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用Kotlin 协程和Flow 以异步、一致的事务方式存储数据。如果您当前在使用SharedPreferences 存储数据,请考虑迁移到 DataStore 。如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用Room,而不是 DataStore 。
2. 分类
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore 。
- Preferences DataStore使用键值对存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
DataStore 是基于 Flow 实现的,不会阻塞主线程。只支持Int ,Long ,Boolean ,Float ,String 键值对数据,适合存储简单、小型的数据,并且不支持局部更新,如果修改了其中一个值,整个文件内容将会被重新序列化。 - Proto DataStore将数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。 Proto DataStore使用协议缓冲区来定义架构。使用协议缓冲区可持久保留强类型数据。与 XML 和其他类似的数据格式相比,协议缓冲区速度更快、规格更小、使用更简单,并且更清楚明了。虽然使用
Proto DataStore 需要学习新的序列化机制,但我们认为 Proto DataStore 有着强大的优势,值得学习。
2.1 Preferences DataStore 和SharedPreferences的区别
SharedPreferences 有一个看上去可以在界面线程中安全调用的同步API ,但是该 API 实际上执行磁盘 I/O 操作。此外,apply() 会阻断fsync() 上的界面线程。每次有服务启动或停止以及每次 activity 在应用中的任何地方启动或停止时,系统都会触发待处理的fsync() 调用。界面线程在apply() 调度的待处理fsync() 调用上会被阻断,这通常会导致ANR。
3. 实践
3.1 Preferences DataStore
3.1.1 依赖
// DataStore Preferences
implementation("androidx.datastore:datastore-preferences:1.0.0")
// optional - RxJava2 support
implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0")
// optional - RxJava3 support
implementation("androidx.datastore:datastore-preferences-rxjava3:1.0.0")
3.1.2 案例
下面案例来源于官网案例,地址:DataStore | Android 开发者 | Android Developers (google.cn)。
class MainActivity2 : AppCompatActivity() {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
private val textView: TextView by lazy { findViewById(R.id.text) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun buttonOnClick(view: View){
runBlocking {
launch {
when(view.id){
R.id.update -> {
incrementCounter()
}
R.id.read -> {
textView.text = getData().toString()
}
}
}
}
}
suspend fun incrementCounter() {
dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
suspend fun getData(): Int {
return dataStore.data.map { settings ->
settings[EXAMPLE_COUNTER] ?: 0
}.first()
}
}
结果:
3.1.3 最后
在前面提到了,这种类型的Preferences DataStore 还支持的数据类型有:Long ,Boolean ,Float ,String 等,不妨看看对应的声明函数:
longPreferencesKey()
booleanPreferencesKey()
floatPreferencesKey()
stringSetPreferencesKey()
对应的,随便找一个方法,看下是如何实现的:
@JvmName("longKey")
public fun longPreferencesKey(name: String): Preferences.Key<Long> = Preferences.Key(name)
也就是其实底层也还是使用Preferences.Key<Long> 来指明类型。和SP 类似数据内容也存储在本地磁盘data/data/packagename/ 中:
3.2 Proto DataStore
对于Preferences DataStore 中的键只能为上述指定的类型,故而如果我们需要存储自定义对象的数据的时候,就显得力不从心了。故而在jetpack 中提供了Proto DataStore 。
3.2.1 依赖
引入datastore 的依赖:
implementation("androidx.datastore:datastore:1.0.0")
// optional - RxJava2 support
implementation("androidx.datastore:datastore-rxjava2:1.0.0")
// optional - RxJava3 support
implementation("androidx.datastore:datastore-rxjava3:1.0.0")
为了使用 Proto DataStore ,让协议缓冲区为我们的架构生成代码,我们需要对 build.gradle 文件进行一些更改:
3.2.1.1 添加协议缓冲区插件
plugins {
...
id "com.google.protobuf" version "0.8.12"
}
3.2.1.2 配置协议缓冲区
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
在dependencies 平级添加:
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
// 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().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
3.2.2 定义架构
在app/src/main/ 目录下创建proto 目录,然后创建一个xxx.proto 文件: testDemo.proto 文件内容:
syntax = "proto3"; // 声明proto的版本
// 定义生成的类的包名
option java_package = "com.weizu.jetpackdemo.proto";
// 声明的是内部类MyProtoBean, 格式: 类型+字段名称+字段编号
message MyProtoBean {
int32 _id = 1;
string _name = 2;
int32 _age = 3;
bool _isMan = 4;
}
至于更加详细的解释可以查阅:protobuf 语言指南以及使用 Proto DataStore (google.cn) 然后Build 一下,就可以看到生成的文件: 打开文件可以看到在该文件中生成了配置中对应的message 类,和对应的set/get 方法: 至此环境配置完毕,接着开始简单使用。
3.2.3 简单使用
3.2.3.1 创建序列化器
我们需要实现序列化器,以告知 DataStore 如何读取和写入我们在 proto 文件中定义的数据类型。如果磁盘上没有数据,序列化器还会定义默认返回值。
object MyBeanSerializer: Serializer<TestDemo.MyProtoBean> {
override val defaultValue: TestDemo.MyProtoBean
get() = TestDemo.MyProtoBean.getDefaultInstance()
override suspend fun readFrom(input: InputStream): TestDemo.MyProtoBean {
try {
return TestDemo.MyProtoBean.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: TestDemo.MyProtoBean, output: OutputStream) {
t.writeTo(output)
}
}
3.2.3.2 数据存储和读取
class MainActivity2 : AppCompatActivity() {
private val textView: TextView by lazy { findViewById(R.id.text) }
private val dataStore: DataStore<TestDemo.MyProtoBean> by lazy {
DataStoreFactory.create(
produceFile = { applicationContext.dataStoreFile("user_prefs.pb") },
serializer = MyBeanSerializer
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private var i = 0
fun buttonOnClick(view: View) {
runBlocking {
launch {
when (view.id) {
R.id.update -> {
storeData(1 + i, 20 + i, true, "张三${i}")
i++
}
R.id.read -> {
val first = readData().first()
Log.e("TAG", "id:${first.id}")
Log.e("TAG", "age:${first.age}")
Log.e("TAG", "isMan:${first.isMan}")
Log.e("TAG", "name:${first.name}")
}
}
}
}
}
private fun readData(): Flow<TestDemo.MyProtoBean> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e("TAG", "Error reading sort order preferences.", exception)
emit(TestDemo.MyProtoBean.getDefaultInstance())
} else {
throw exception
}
}
}
private suspend fun storeData(id: Int, age: Int, isMan: Boolean, name: String) {
dataStore.updateData { preferences ->
preferences.toBuilder()
.setAge(age)
.setId(id)
.setIsMan(isMan)
.setName(name)
.build()
}
}
}
结果:
每次插入数据都会覆盖掉以前的数据,也就是在使用对象存储数据的时候,只能存储一条数据。
|