DataStore的基础用法
Posted microhex
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DataStore的基础用法相关的知识,希望对你有一定的参考价值。
文章目录
0. 简介
Google在推出JetPack
组件以来,一直推荐我们使用DataStore
组件替代到我们第一天学android就知道的SharedPreferences
组件,原因很简单,因为当年的SharedPreferences
存在居多的问题,DataStore
就是为了解决这些问题而来的。
1. SP的缺点
至于 SP
到底存在哪些问题,我们可以直接查看 DataStore
源码上的注释:
- Synchronous API encourages StrictMode violations
- apply() and commit() have no mechanism of signalling errors
- apply() will block the UI thread on fsync()
- Not durable – it can returns state that is not yet persisted
- No consistency or transactional semantics
- Throws runtime exception on parsing errors
- Exposes mutable references to its internal state
用我们蹩脚的英语逐字逐句的翻译一遍:
- 同步的API鼓励违反StrictMode模式
- apply()和commit()方法没有错误信号机制
- apply()方法将界面重绘时会在阻塞UI线程
- 不耐用 - 它可以返回状态,但是并不能将状态持久化
- 没有一致性或者事物语义
- 解析出现错误时,直接抛出运行时异常
- 在其内部的状态中,暴露其可变的引用
老外写的问题直译过来一般都比较难懂,除非讲的很简单清楚。那么我就说两句人话,大概说一下我所认为的 SP
所存在的问题吧:
- 不支持跨进程,使用
MODE_MULTI_PROCESS
模式也没鸟用。而且在跨进程中,频繁的读写可能导致数据损坏或者丢失;- 懒加载模式下读取
SP
文件,可能会导致 getXXX() 阻塞。所以建议提前异步初始化SP
;sp
文件的中的数据全部都保存在内存中,所以SP
对大数据量少儿不宜edit()
方法每次都会新建一个EditorImpl
对象。建议一次edit()
,多次putXXX
;- 无论是
commit()
还是apply()
,针对任何修改都是全量写入。这种情况下,对于高频的修改配置项存放在单独的SP
文件中;commit()
同步保存,有返回值;apply()
异步保存,无返回值。onPause()
onReceive()
方法中使用异步写操作执行完成,可能会造成卡顿或者ANR。
当然这里并不是把SP
贬得一无是处啊,正所谓存在即合理,当我们不涉及到跨进程,并且存储数据量比较少的情况下,SP
还是相当不错的选择。
2. DataStore的基础用法
首先需要声明的一点是,DataStore
存在两个版本的,一种是类似于SP
,基于普通文件的读写;一种是基于Google protobuf
模式的,这里的 protobuf
是 Google
自研的一种数据结构,平时用到的也比较少,我以前博客里面也写过类似的,这里就先只介绍第一种基于文件的逻辑:
下面介绍一下 DataStore
的基础用法:
首先需要引入:
implementation("androidx.datastore:datastore:1.0.0")
首先需要明确一点,既然我们的DataStore
是兼容当前使用的SP
的,那么它就应该支持SP
的存储类型,而且我们也知道SP
支持的数据类型为Int
,Long
,Float
,Boolean
,String
和StringSet
;此时DataStore
不仅支持以上六种数据结构,还支持一种额外的Double
类型。
创建一个DataStore
对象:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "dataStore_data")
首先我们需要读取一个Int
型的对象:
val keyName = intPreferencesKey("example_counter")
val keyNameFlow: Flow<Int> = context.dataStore.data
.map preferences -> preferences[keyName] ?: 0
然后我们写入一个 Int
型的对象:
val keyName = intPreferencesKey("example_counter")
suspend fun incrementCounter()
context.dataStore.edit settings ->
val currentCounterValue = settings[keyName] ?: 0
settings[keyName] = currentCounterValue + 1
第一眼看上去很懵逼,写的什么玩意儿啊。没错,我学 DataStore
的第一天也是这么想法,SP
比这玩意儿香一万倍都不止啊,这么多新的东西我不知道,而且感觉写起来也是很拉跨,写一个简单的存储这么多代码。
首先,的确一个新的知识点出来,大家内心肯定是抗拒的,因为要去理解和实践,这个本身就比较耗时间和精力。但是大家都一样,所以还是需要去迎接变化。
首先,我们按照简单的来,我们都知道SP
是基于XML
文件的Key-Value
结构,那么 DataStore
作为它的兼容类,也必然兼容这种Key-Value
结构。那么DataStore
的Key
是String
型的么?然而并不是,它是一种Preferences.Key
类型,具体类型为:androidx.datastore.preferences.core.Preferences.Key
, 可以分为以下几种类型:
- intPreferencesKey -> Preferences.Key<Int> 保存
Int
类型数据- doublePreferencesKey -> Preferences.Key<Double> 保存
Double
类型数据- stringPreferencesKey -> Preferences.Key<String> 保存
String
类型数据- booleanPreferencesKey ->Preferences.Key<Boolean> 保存
Boolean
类型数据- floatPreferencesKey -> Preferences.Key<Float> 保存
Float
类型数据- longPreferencesKey -> Preferences.Key<Long> 保存
Long
类型数据- stringSetPreferencesKey -> Preferences.Key<Set<String>> 保存
Set<String>
类型数据
有了Key
之后,我们需要看看DataStore
如果存储和读取数据的。
a. DataStore怎么写
SP
有Editor
,同理DataStore
也有edit
方法:
public suspend fun DataStore<Preferences>.edit(
transform: suspend (MutablePreferences) -> Unit
): Preferences
return this.updateData
// It's safe to return MutablePreferences since we freeze it in
// PreferencesDataStore.updateData()
it.toMutablePreferences().apply transform(this)
首先它是一个suspend
函数,只能在协程体中运行,每当遇到 suspend
函数以挂起的方式运行时,并不会阻塞主线程运行。
既然是suspend
函数,那么我们就可以有同步
和异步
的方式对数据进行写入:
同步方式
:
private suspend fun saveSyncIntData(key : String, value:Int)
globalDataStore.edit mutablePreferences -> mutablePreferences[intPreferencesKey(key)] = value
异步方式
:
这个就很简单了,可以随意发挥了,在同步方法上套一个runBlocking
就行了:
private fun saveIntData(key: String, value:Int) = runBlocking saveSyncIntData(key,value)
b. DataStore怎么读
按照以上的惯例,肯定也会存在同步读取
和异步读取
的两种方法。首先需要明确一点,DataStore
的data
返回的是Flow
类型,Flow
是一种流式接口,类似于RxJava
中的 Observable
那样,存在很多操作符可以对数据进行变换,时间允许的情况下,可以写一篇关于Flow
文章。
首先我们获取到同步的读
:
private fun readSyncIntData(key: String, defaultValue: Int) : Flow<Int> = dataStore.data.catch
if(it is IOException)
it.printStackTrace()
emit(emptyPreferences())
else
throw it
.map it[intPreferencesKey(key)] ?: defaultValue
对代码进行解读一下, dataStore.data
返回类型是Flow
类型,对Flow
进行catch
检查是否存在异常,然后map
转换一下,然后得到Flow<Int>
,最终并返回。
写完了同步的读
,那么异步的读
为:
private fun readIntData(key: String, defaultValue : Int) : Int
var resultValue = defaultValue
runBlocking
dataStore.data.first
resultValue = it[intPreferencesKey(key)] ?: resultValue
true
return resultValue
异步的读取直接返回了具体类型的数据,这里的first
操作符是取第一个的意思。
基本上,我们对DataStore
的操作有了一个简单的了解,重要的还是自己去实践,不难也不算容易。
3. 从SP迁移到DataStore
大概也就分两个步骤:
- 需要一个
SharedPreferencesMigration
,这个迁移类并不算难,也就需要你传入Context
和SP
的文件名即可:
val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = "dataStore_setting",
produceMigrations = context ->
listOf( SharedPreferencesMigration(context, "sp_name"))
)
- 当
dataStore
生成完成之后,需要执行一个读或者写的操作,SharedPreferences
的数据将会被迁移到dataStore
中,同时SharedPreferences
文件也将会被删除。
-
使用
SP
的文件夹
-
迁移到
DataStore
的文件夹
可以看到SP
文件被删除了,然后dataStore
的文件目录为/data/data/package_name/files/xxx.preferences_pb
4. DataStore的封装类
为了方便操作,我这边封装了DataStore
的逻辑,读写起来会更方便一点,方法部分代码为:
我们使用时也很简单,直接代码为:
DataStoreUtils.putData("int_value",100)
DataStoreUtils.putData("long_value",100L)
DataStoreUtils.putData("float_value",100.0f)
DataStoreUtils.putData("double_value",100.00)
DataStoreUtils.putData("boolean_value",true)
DataStoreUtils.putData("string_value","hello world")
val intValue = DataStoreUtils.getData("int_value", 0)
val longValue = DataStoreUtils.getData("long_value", 0L)
val floatValue = DataStoreUtils.getData("float_value", 0.0f)
val doubleValue = DataStoreUtils.getData("double_value", 0.00)
val booleanValue = DataStoreUtils.getData("boolean_value", false)
val stringValue = DataStoreUtils.getData("string_value", "hello")
当然,这只是异步的读取/存储方式,当然我们还有同步的获取方式:
lifecycle.coroutineScope.launch
// 读取
DataStoreUtils.getSyncData("int_value",0).collect(object : FlowCollector<Int>
override suspend fun emit(value: Int)
Log.d("TAG","get sync data : $value")
)
// 写入
DataStoreUtils.putSyncData("int_value", 1)
当然,具体的源码可以看这个了.
5. 个人结论
总体来说,DataStore
如果高度封装,其实使用方式上和SP
基本上没什么区别,它解决了SP
所存在的诟病,但是就目前而言,对它的性能还是未知的,这个可能需要后续的线上检验了,当然谷爹出品的东西,应该没什么太大的问题。当然了,学习DataStore
其实对Kotlin
还是有很高的门槛的,其中协程
,高阶函数
、Flow
等相关知识点还是存在一个相当陡峭的学习坡度的。
以上是关于DataStore的基础用法的主要内容,如果未能解决你的问题,请参考以下文章
GCP Datastore Python - InvalidArgument:400 非事务性提交可能不包含影响同一实体的多个突变