什么是DataStore
在官网中对它的描述
一个数据存储解决方案。使用Kotlin协程和Flow异步、一致和事务性地存储数据
并且很贴心的建议 「如果您目前使用SharedPreferences存储数据,请考虑迁移到DataStore」
。
从官网的描述,大致能够知道 DataStore
干的事情和SharedPreferences
差不多。都是对少量的数据进行存储。
为什么会有DataStore
在官网中,明确建议我们迁移到DataStore
,而不是继续使用SharedPreferences
。
先放上一个官方的总结
功能 | SharedPreferences | Preferences DataStore | Proto DataStore |
---|---|---|---|
异步 | ✅(仅用于读取更改的值) 通过 listener | ✅ 通过 Flow | ✅ 通过 Flow |
同步 | ✅ | ❌ | ❌ |
安全调用UI线程 | ❌ | ✅ | ✅ |
发出错误信号 | ❌ | ✅ | ✅ |
不受运行时异常的影响 | ❌ | ✅ | ✅ |
具有强一致性保证的事务性Api | ❌ | ✅ | ✅ |
处理数据迁移 | ❌ | ✅ | ✅ |
类型安全 | ❌ | ❌ | ✅ |
表格来源 Android 官网
通过上面的总结。可以看到。SharedPreferences
相比于DataStore
存在着不少的安全隐患。Google在Jetpack中,给笔者的感觉就是尽量将错误都给收到内部,从Kotlin的语法糖。到ViewBinding
对于一些安全隐患尤为看中,对于开发者来说。也是件好事情。
Preferences DataStore如何使用
在官网中,DataStore提供了两种不同的实现
Preferences DataStore
使用键存储和访问数据。此实现不需要预定义的架构,并且不提供类型安全性。Proto DataStore
将数据存储为自定义数据类型的实例。此实现要求您使用协议缓冲区定义架构,但它提供类型安全性。
这一小节先说一下 Preferences DataStore 如何使用。
添加依赖
笔者针对 Preferences DataStore
归纳总结其使用。首先肯定是添加依赖
// Preferences DataStore (SharedPreferences like APIs)
dependencies {
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha05"
}
// 无Android环境下使用.
dependencies {
implementation "androidx.datastore:datastore-preferences-core:1.0.0-alpha05"
}
Android开发者其实只需要添加上面一行即可。目前最新版本依然是alpha
版。项目中还是慎用比较好。
创建DataStore
一样从官网中得知。先创建一个DataStore,使用的是Context的拓展函数createDataStore
,其中name
为必填项。
val dataStore: DataStore<Preferences> = context.createDataStore(
name = "allens_form_ds"
)
和SharePreferences一样,name对应的一个文件的名称,如下图,可以看到其对应的文件。
添加属性
既然Datastore对标的是SharePreferences。我们先想一下SharePreferences如何添加属性的
getSharedPreferences("allens_form_sp", Context.MODE_PRIVATE)
.edit()
.putString("name", "在雨季等你")
.putInt("age", 22)
.commit()
在看一下DataStore。
GlobalScope.launch {
//获取DataStore 对标 getSharedPreferences
val dataStore: DataStore<Preferences> = createDataStore(
name = "allens_form_ds"
)
//创建key
val userName = preferencesKey<String>("name")
val userAge = preferencesKey<Int>("age")
dataStore
.edit { value ->
value[userName] = "在雨季等你"
value[userAge] = 22
}
}
与SharePreferences区别
使用上几乎和SharePreferences一样。需要注意的点
- key 需要使用
preferencesKey
或者preferencesSetKey
去创建 - edit是一个挂起函数,需要放在协程中去使用
与SharePreferences相同点
和SharePreferences相同的点。都只支持基本的数据类型,在preferencesKey
方法中也能窥视
public inline fun <reified T : Any> preferencesKey(name: String): Preferences.Key<T> {
return when (T::class) {
Int::class -> {
Preferences.Key<T>(name)
}
String::class -> {
Preferences.Key<T>(name)
}
Boolean::class -> {
Preferences.Key<T>(name)
}
Float::class -> {
Preferences.Key<T>(name)
}
Long::class -> {
Preferences.Key<T>(name)
}
Double::class -> {
Preferences.Key<T>(name)
}
Set::class -> {
throw IllegalArgumentException("Use `preferencesSetKey` to create keys for Sets.")
}
else -> {
throw IllegalArgumentException("Type not supported: ${T::class.java}")
}
}
}
获取属性
DataStore获取属性使用的是Flow
流的方式,对其不了解的小伙伴可以先花了10分钟看一下官网对着一小节的示例,有个大致的印象点击查看异步流
dataStore.data
方法返回就是一个Flow.可自行对Flow进行处理。通过key获取我们想获取的属性,或者对Flow进行map等其他的操作将保存的属性进行转换等。
- 获取某一个属性
dataStore.data
//可通过Flow collect 属性获取流在流中进行判断
.collect {
//通过传入key去获取对应的value
println("user name:${it[userName]}")
}
- 获取多个属性
dataStore.data
.collect {
println("user name:${it[userName]}")
println("user age:${it[userAge]}")
}
还可以这样使用Flow的onEach
操作符去处理
dataStore.data
.onEach {
println("user name:${it[userName]}")
println("user age:${it[userAge]}")
}
.collect()
- 异常处理
异常控制是属于Flow控制流的东西。对于本文来说,有点拖沓,笔者这里也就简单带过。
dataStore.data
.onEach {
println("user name:${it[userName]}")
println("user age:${it[userAge]}")
//模拟异常
throw NullPointerException()
}
//处理异常
.catch { println("error $it") }
//成功失败都走这里。类似于finly
.onCompletion { println("complete") }
.collect()
总之通过Flow可以做到很多事情,可以转换使用map等。依然建议先看一下Flow在上手DataStore
Proto DataStore使用
在使用Proto DataStore
之前,还是有一个问题,既然有了Preferences DataStore
还要他干嘛。
为什么要有Proto DataStore
其实在上面小节学习Preferences DataStore
的时候,我们发现其只支持一些基本的数据类型,无法做到自定义类型的存储,Proto DataStore
就是解决这个问题而诞生的。
配置依赖
Proto DataStore
相比于Preferences DataStore
,使用还是比较复杂的。下面笔者也将其配置过程整理如下。方便读者去使用和配置
- 添加
Protobuf
插件
在app
的build.gradle
中配置插件。如果依赖了其他module,那么子module也需要完成下面配置.
plugins {
id "com.google.protobuf" version "0.8.12"
}
- 添加依赖
dependencies {
//Android 只需要添加这一个就够了
implementation "androidx.datastore:datastore:1.0.0-alpha05"
//Protobuf依赖
implementation "com.google.protobuf:protobuf-javalite:3.14.0"
}
- 配置Protobuf
在build.gradle
中添加下列节点。注意是和android
同一级节点
//配置Protobuf
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'
}
}
}
}
}
创建Protobuf对象
在app/src/main/proto
下创建user_prefs.proto
文件
//语言版本
syntax = "proto3";
//包名
option java_package = "com.allens.okdatastore";
option java_multiple_files = true;
//每一个新的类型用 message 表示
message UserPreferences {
bool show_completed = 1;
int32 age = 2;
string name = 3;
float price = 4;
}
完成以后一定要rebuild
一下。编译器会帮我创建好可用的Java对象
有读者会发现,自己写的这个user_prefs.proto
可读性非常差,这里也推荐Android Studio的一个插件
代码看起来不会那么难看。好歹是有个颜色~~
Proto语言
在上面定义的user_prefs.proto
文件使用到的是Proto语言
,对于Android开发来说,不需要掌握那么深。详细的信息可以查看Proto语言指南
Protocol Buffers (ProtocolBuffer/ protobuf )是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。同XML相比,Protocol buffers在序列化结构化数据方面有许多优点:
- 更简单
- 数据描述文件只需原来的1/10至1/3
- 解析速度是原来的20倍至100倍
- 减少了二义性
- 生成了更容易在编程中使用的数据访问类
序列化
为了告诉DataStore如何读写在原始文件中定义的数据类型,我们需要实现一个Serializer
。创建一个新的类UserPreferencesSerializer
注意了,如果在上面没有rebuild
,可能会找不到UserPreferences
这个类
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
写入数据
val dataStore = createDataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer
)
dataStore.updateData { preferences ->
preferences.toBuilder()
.setShowCompleted(false)
.setAge(22)
.setName("江海洋")
.setPrice(10.5f)
.build()
}
读取数据
val dataStore = createDataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer
)
dataStore.data
.collect {
println("age:${it.age}")
println("name:${it.name}")
println("price:${it.price}")
println("completed:${it.showCompleted}")
}
SharedPreferences迁移
使用起来也非常简单
createDataStore(
//迁移到DataStore的名字
name = "user",
migrations = listOf(
SharedPreferencesMigration(
context = this@MainActivity,
//原来SharedPreferences的名字
sharedPreferencesName = "user"
)
)
)
需要注意的是执行完成以后不是立马就能迁移成功的,需要执行一次读取或者写入操作
才会生效。迁入成功后会删除原有的SharedPreferences
对于DataStore的一些看法
优点
首先一点。笔者非常认同DataStore的设计理念,「不在单纯像SharedPreferences
一样去设置一个String
类型的参数作为key」。而是强制使用preferencesKey
将key与设置的类型
强行进行关联可以发现。google一直在想着帮助开发者去减少因为错误类型而带来的crash。
其次引入Flow
作为事件流去使用。能够帮助开发者更好的处理事件。像写RxJava那样爽。
最后呢,DataStore方法强制要求在协程中去使用。也能够避免阻塞线程做到了安全调用UI线程
的功能
基于上诉3点,笔者在学习过程中认为其设计思想,以及对SharedPreferences
的0成本迁移。确实可以做到替换SharedPreferences
缺点
咋也不能一个劲的夸她,毕竟到目前为止还是alpha
。对比SharedPreferences
可以说除了API相对来说难用一点,基本上完胜。那和MMKV
相比呢?
熟悉MMKV
的读者可能知道。(不熟悉的读者可以去看一下官网的介绍MMKV中文文档)。
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强
而DataStore
在上面我们可看到了,本质上还是以文件的形式去存储。而且在使用自定义属性的时候。要去写Proto语言
.也是比较麻烦的。使用MMKV只需要一行代码即可。这一点完败。更可恨的是MMKV也能够支持SharedPreferences
迁移。
对比实际开发项目来说,笔者认为MMVK的优势确实要比DataStore要好。其API以及其mmap的方式更佳适合做少量数据存储。
实际中发现DataStore的问题(这里误认子弟了)
2021-1-22 更新
这里误认子弟了。这个设计是非常好的设计。是笔者自己的思想还停留在 SharedPreferences
的层面。如同DataStore
的作者已经在了第10层。我还在第2层。去评判这个库。下面是其作者回复。
数据存储。数据是一个不应该完成的流。它应该在每次数据存储中发生更改时发出。
本身Flow会监听数据的变化。而笔者误认为是一个错误。丢脸了。
笔者这边先写一个小示例
suspend fun main() {
(1..3).asFlow()
.onCompletion { println("完成") }
.collect { println("$it") }
}
上面的代码执行,笔者相信大家都知道
1
2
3
完成
然后Flow流程应该是结束的。但是在1.0.0-alpha05
版本的DataStore实际上发现Flow无法结束
🌰
笔者先在创建一个DataStore并且添加一个name
属性.这里就不放代码了,然后去查询name
,(笔者这里使用runBlocking
测试使用。实际项目不要这么用)
viewBinding.linear.addView(createButton("Test") {
runBlocking {
val dataStore: DataStore<Preferences> = createDataStore(
name = "test"
)
val key = preferencesKey<String>("name")
dataStore.data
.onCompletion { println("完成") }
.collect { println("${it[key]}") }
}
})
按照想法,应该是打印出name
然后打印完成
,和上面的123完成
一样才对。可实际结果是,打印name
以后协程一直挂起,也不会结束。 具体原因还不清楚。知道的小伙伴可以告知一下。
OKDataStore
上面都在介绍DataStore的使用。下面介绍一下笔者针对DataStroe
的一些小封装。笔者发现。DataStore
最初的设计是将返回类型与key强项绑定。防止发生错误。初衷是好的,但是却牺牲了便捷性。即便是使用MMKV
,只需要一个String类型的key即可去保存属性,笔者认为,如果只是简单的数据类型保存。不需要那么可以去使用,反而有点画蛇添足。
保存数据
OKDataStore
就仿造SharedPreferences
去添加和保存数据,用法如下。
runBlocking {
createOkDataStore("user")
.edit()
.putString("name", "江海洋")
.putInt("age", 21)
.putBoolean("isBody", true)
.putFloat("size", 20f)
.putLong("long", 100L)
.putStringSet("setData", setOf("a", "b", "c", "d"))
.commit()
}
对于自定义的类型,笔者这边依然保留了Proto DataStore
的使用方式。考虑到实际项目中,真要有一些自定义的属性,可完全可以使用json的形式去保存String.所以也对这一块不太感冒
runBlocking {
val dataStore = createOkDataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer
)
dataStore.updateData { preferences ->
preferences.toBuilder()
.setShowCompleted(false)
.setAge(22)
.setName("江海洋")
.setPrice(10.5f)
.build()
}
}
获取数据
获取数据,笔者这边考虑到其实一般来说想获取的数据不会那么复杂,但是又有需求可能需要设置默认的选项,毕竟SharedPreferences
和MMKV
都是可以这么设置的。这么设计的好处不言而喻,故此,在OKDataStore
中也加入了默认选项。并且可以去获取指定类型。依然保留的Flow
,笔者认为,Flow
在kotlin
中还是比较好用的。
runBlocking {
createOkDataStore("user")
.getInt("age", 10000)
.collect {
println("data:$it")
}
}
如果是错误类型可以使用catch
操作符去处理,例如
runBlocking {
createOkDataStore("user")
//故意写错 这样会出现类型错误
.getInt("name", 10000)
.catch {
println("获取name error:${it.message}")
}
.collect {
println("data:$it")
}
}
也可以直接使用Flow
的特性自行处理所有结果,其效果和datastore.data
是一样的。读者可以自行去处理,不过依然有上面小节说展示的问题Flow无法结束
runBlocking {
createOkDataStore("user")
.flow()
.collect {
println("data:$it")
}
}
获取自定义类型,依然沿用了DataStore
的方式
runBlocking {
val dataStore = createOkDataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer
)
dataStore.flow()
.collect {
println("age:${it.age}")
println("name:${it.name}")
println("price:${it.price}")
println("completed:${it.showCompleted}")
}
}
为了解决DataStore
无法结束Flow的问题。笔者这边自定义了一个Throwable
去中断Flow
,当然这个选项也是可以关闭的,默认是开启的,代码如下。只需要将cancel
设置成false
即可关闭,读者需要自定处理Flow带来的问题。至于为什么会这样。笔者这里也不是很清楚。BUG? 还是设计如此? 有知道的小伙伴可以告知!
fun Context.createOkDataStore(
name: String,
cancel: Boolean = true,
migrations: List<DataMigration<Preferences>> = listOf()
): OKDataStore {
return OKDataStoreImpl(name = name, context = this, cancel = cancel, migrations = migrations)
}
设计思路
笔者发现,DataStore
设计中,其实只有一个事件流,然后返回一个Preferences
。在Preferences
通过key在获取保存的参数,笔者这边将这个事件流通过flatMapConcat
操作符去变成多个事件流。然后再去处理对应的流。
private fun <T> getValueFormKey(key: String, default: T): Flow<T> {
return dataStore.data
.map { it.asMap() }
.flatMapConcat { requestFlow(it, key, default) }
.cancellable()
.map { it.second as T }
}
但是毕竟坑的就是flatMapConcat
操作符,处于预览状态。唉,天知道后续会咋样。
最后放上项目源码,大家可以看一下。OKDataStore